@phren/cli 0.0.28 → 0.0.33

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 (153) 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} +25 -21
  7. package/mcp/dist/{cli.js → cli/cli.js} +13 -13
  8. package/mcp/dist/{cli-config.js → cli/config.js} +12 -12
  9. package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
  10. package/mcp/dist/{cli-govern.js → cli/govern.js} +28 -17
  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} +58 -117
  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} +12 -11
  21. package/mcp/dist/cli-hooks-git.js +243 -0
  22. package/mcp/dist/cli-hooks-prompt.js +323 -0
  23. package/mcp/dist/cli-hooks-session-handlers.js +337 -0
  24. package/mcp/dist/cli-hooks-stop.js +519 -0
  25. package/mcp/dist/{content-archive.js → content/archive.js} +16 -29
  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} +41 -20
  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} +142 -15
  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} +13 -7
  41. package/mcp/dist/governance/audit.js +30 -0
  42. package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
  43. package/mcp/dist/{governance-policy.js → governance/policy.js} +23 -12
  44. package/mcp/dist/{governance-rbac.js → governance/rbac.js} +4 -4
  45. package/mcp/dist/{governance-scores.js → governance/scores.js} +10 -11
  46. package/mcp/dist/hooks.js +53 -37
  47. package/mcp/dist/index-query.js +4 -1
  48. package/mcp/dist/index.js +54 -30
  49. package/mcp/dist/{init-config.js → init/config.js} +6 -6
  50. package/mcp/dist/{init.js → init/init.js} +80 -69
  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} +4 -4
  54. package/mcp/dist/init-bootstrap.js +21 -0
  55. package/mcp/dist/init-detect.js +38 -0
  56. package/mcp/dist/init-env.js +114 -0
  57. package/mcp/dist/init-fresh.js +234 -0
  58. package/mcp/dist/init-hooks.js +26 -0
  59. package/mcp/dist/init-mcp.js +65 -0
  60. package/mcp/dist/init-modes.js +135 -0
  61. package/mcp/dist/init-npm.js +37 -0
  62. package/mcp/dist/init-project-local.js +99 -0
  63. package/mcp/dist/init-semantic.js +48 -0
  64. package/mcp/dist/init-types.js +1 -0
  65. package/mcp/dist/init-uninstall.js +504 -0
  66. package/mcp/dist/init-update.js +96 -0
  67. package/mcp/dist/init-walkthrough.js +524 -0
  68. package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
  69. package/mcp/dist/{link-context.js → link/context.js} +4 -4
  70. package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
  71. package/mcp/dist/{link.js → link/link.js} +26 -31
  72. package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
  73. package/mcp/dist/logger.js +11 -3
  74. package/mcp/dist/package-metadata.js +1 -1
  75. package/mcp/dist/phren-art.js +4 -126
  76. package/mcp/dist/phren-paths.js +30 -12
  77. package/mcp/dist/proactivity.js +3 -3
  78. package/mcp/dist/profile-store.js +5 -6
  79. package/mcp/dist/project-config.js +2 -2
  80. package/mcp/dist/project-topics.js +17 -47
  81. package/mcp/dist/provider-adapters.js +1 -1
  82. package/mcp/dist/query-correlation.js +1 -1
  83. package/mcp/dist/runtime-profile.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} +28 -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} +19 -42
  90. package/mcp/dist/shared/governance.js +4 -0
  91. package/mcp/dist/{shared-index.js → shared/index.js} +105 -132
  92. package/mcp/dist/{shared-ollama.js → shared/ollama.js} +25 -7
  93. package/mcp/dist/shared/process.js +24 -0
  94. package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +22 -24
  95. package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +18 -20
  96. package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
  97. package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
  98. package/mcp/dist/shared.js +6 -60
  99. package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
  100. package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
  101. package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
  102. package/mcp/dist/{shell-render.js → shell/render.js} +2 -2
  103. package/mcp/dist/{shell.js → shell/shell.js} +11 -11
  104. package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
  105. package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
  106. package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
  107. package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
  108. package/mcp/dist/{skill-registry.js → skill/registry.js} +5 -5
  109. package/mcp/dist/{skill-state.js → skill/state.js} +1 -4
  110. package/mcp/dist/startup-embedding.js +2 -2
  111. package/mcp/dist/status.js +15 -14
  112. package/mcp/dist/{tasks-github.js → task/github.js} +3 -2
  113. package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
  114. package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +8 -13
  115. package/mcp/dist/telemetry.js +3 -4
  116. package/mcp/dist/tool-registry.js +29 -17
  117. package/mcp/dist/tools/config.js +530 -0
  118. package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
  119. package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
  120. package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
  121. package/mcp/dist/tools/finding.js +584 -0
  122. package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
  123. package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
  124. package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
  125. package/mcp/dist/tools/ops.js +468 -0
  126. package/mcp/dist/tools/search.js +672 -0
  127. package/mcp/dist/{mcp-session.js → tools/session.js} +51 -25
  128. package/mcp/dist/{mcp-skills.js → tools/skills.js} +42 -35
  129. package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
  130. package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
  131. package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
  132. package/mcp/dist/{memory-ui-page.js → ui/page.js} +5 -7
  133. package/mcp/dist/ui/server.js +1024 -0
  134. package/mcp/dist/update.js +2 -2
  135. package/mcp/dist/utils.js +63 -19
  136. package/package.json +2 -2
  137. package/scripts/preuninstall.mjs +31 -0
  138. package/starter/global/CLAUDE.md +3 -2
  139. package/mcp/dist/governance-audit.js +0 -22
  140. package/mcp/dist/mcp-config.js +0 -551
  141. package/mcp/dist/mcp-finding.js +0 -594
  142. package/mcp/dist/mcp-ops.js +0 -363
  143. package/mcp/dist/mcp-search.js +0 -668
  144. package/mcp/dist/memory-ui-server.js +0 -1411
  145. package/mcp/dist/shared-governance.js +0 -4
  146. /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
  147. /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
  148. /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
  149. /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
  150. /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
  151. /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
  152. /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
  153. /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
@@ -1,1411 +0,0 @@
1
- import * as http from "http";
2
- import * as crypto from "crypto";
3
- import { timingSafeEqual } from "crypto";
4
- import * as fs from "fs";
5
- import * as path from "path";
6
- import * as querystring from "querystring";
7
- import { spawn, execFileSync } from "child_process";
8
- import { computePhrenLiveStateToken, getProjectDirs, } from "./shared.js";
9
- import { editFinding, readReviewQueue, removeFinding, readFindings, addFinding as addFindingStore, readTasksAcrossProjects, addTask as addTaskStore, completeTask as completeTaskStore, removeTask as removeTaskStore, updateTask as updateTaskStore, TASKS_FILENAME, } from "./data-access.js";
10
- import { isValidProjectName, errorMessage, queueFilePath, safeProjectPath } from "./utils.js";
11
- import { readInstallPreferences, writeInstallPreferences, writeGovernanceInstallPreferences } from "./init-preferences.js";
12
- import { buildGraph, collectProjectsForUI, collectSkillsForUI, getHooksData, isAllowedFilePath, readSyncSnapshot, recentAccepted, recentUsage, } from "./memory-ui-data.js";
13
- import { CONSOLIDATION_ENTRY_THRESHOLD } from "./content-validate.js";
14
- import { ensureTopicReferenceDoc, getProjectTopicsResponse, listProjectReferenceDocs, pinProjectTopicSuggestion, readReferenceContent, reclassifyLegacyTopicDocs, unpinProjectTopicSuggestion, writeProjectTopics, } from "./project-topics.js";
15
- import { getWorkflowPolicy, updateWorkflowPolicy, mergeConfig, getRetentionPolicy, getProjectConfigOverrides, VALID_TASK_MODES } from "./governance-policy.js";
16
- import { readProjectConfig, updateProjectConfigOverrides } from "./project-config.js";
17
- import { findSkill } from "./skill-registry.js";
18
- import { setSkillEnabledAndSync } from "./skill-files.js";
19
- import { repairPreexistingInstall } from "./init-setup.js";
20
- const CSRF_TOKEN_TTL_MS = 15 * 60 * 1000;
21
- const MAX_FORM_BODY_BYTES = 1_048_576;
22
- const WEB_UI_READY_ATTEMPTS = 12;
23
- const WEB_UI_READY_DELAY_MS = 75;
24
- const WEB_UI_PORT_RETRY_ATTEMPTS = 3;
25
- export function getWebUiBrowserCommand(url, platform = process.platform) {
26
- if (platform === "darwin")
27
- return { command: "open", args: [url] };
28
- if (platform === "win32")
29
- return { command: process.env.ComSpec || "cmd.exe", args: ["/c", "start", "", url] };
30
- return { command: "xdg-open", args: [url] };
31
- }
32
- export async function launchWebUiBrowser(url) {
33
- const { command, args } = getWebUiBrowserCommand(url);
34
- await new Promise((resolve, reject) => {
35
- try {
36
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
37
- child.once("error", reject);
38
- child.once("spawn", () => {
39
- child.removeListener("error", reject);
40
- child.unref();
41
- resolve();
42
- });
43
- }
44
- catch (err) {
45
- reject(err);
46
- }
47
- });
48
- }
49
- export async function waitForWebUiReady(url, attempts = WEB_UI_READY_ATTEMPTS, delayMs = WEB_UI_READY_DELAY_MS) {
50
- for (let attempt = 0; attempt < attempts; attempt++) {
51
- const ready = await new Promise((resolve) => {
52
- const req = http.get(url, (res) => {
53
- res.resume();
54
- resolve(true);
55
- });
56
- req.setTimeout(1000, () => {
57
- req.destroy();
58
- resolve(false);
59
- });
60
- req.on("error", () => resolve(false));
61
- });
62
- if (ready)
63
- return true;
64
- if (attempt < attempts - 1) {
65
- await new Promise((resolve) => setTimeout(resolve, delayMs));
66
- }
67
- }
68
- return false;
69
- }
70
- function isAddressInUse(err) {
71
- return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
72
- }
73
- async function listenOnLoopback(server, port) {
74
- return await new Promise((resolve, reject) => {
75
- const onError = (err) => {
76
- server.off("listening", onListening);
77
- reject(err);
78
- };
79
- const onListening = () => {
80
- server.off("error", onError);
81
- const address = server.address();
82
- if (!address || typeof address === "string") {
83
- reject(new Error("failed to determine web-ui port"));
84
- return;
85
- }
86
- resolve(address.port);
87
- };
88
- server.once("error", onError);
89
- server.once("listening", onListening);
90
- server.listen(port, "127.0.0.1");
91
- });
92
- }
93
- async function bindWebUiPort(server, requestedPort, allowPortFallback) {
94
- const candidates = [requestedPort];
95
- if (allowPortFallback && requestedPort > 0) {
96
- for (let i = 1; i <= WEB_UI_PORT_RETRY_ATTEMPTS; i++) {
97
- candidates.push(requestedPort + i);
98
- }
99
- }
100
- let lastError = null;
101
- for (let i = 0; i < candidates.length; i++) {
102
- const candidate = candidates[i];
103
- try {
104
- if (candidate !== requestedPort) {
105
- process.stderr.write(`[phren] web-ui port ${candidate - 1} is busy, retrying on ${candidate}\n`);
106
- }
107
- return await listenOnLoopback(server, candidate);
108
- }
109
- catch (err) {
110
- lastError = err;
111
- if (!allowPortFallback || !isAddressInUse(err) || i === candidates.length - 1)
112
- break;
113
- }
114
- }
115
- throw lastError instanceof Error ? lastError : new Error("failed to bind web-ui server");
116
- }
117
- function pruneExpiredCsrfTokens(csrfTokens) {
118
- if (!csrfTokens)
119
- return;
120
- const now = Date.now();
121
- for (const [token, createdAt] of csrfTokens) {
122
- if (now - createdAt > CSRF_TOKEN_TTL_MS)
123
- csrfTokens.delete(token);
124
- }
125
- }
126
- function setCommonHeaders(res, nonce) {
127
- res.setHeader("Referrer-Policy", "no-referrer");
128
- if (nonce) {
129
- // Page responses: allow nonce-gated inline scripts but disallow inline event handlers
130
- res.setHeader("Content-Security-Policy", `default-src 'self'; script-src 'self' 'nonce-${nonce}' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.bunny.net; font-src https://fonts.bunny.net; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'`);
131
- }
132
- else {
133
- // API responses: no inline scripts needed
134
- res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'");
135
- }
136
- res.setHeader("X-Content-Type-Options", "nosniff");
137
- res.setHeader("X-Frame-Options", "DENY");
138
- }
139
- function getSubmittedAuthToken(req, url, parsedBody) {
140
- const authHeader = req.headers.authorization;
141
- if (typeof authHeader === "string") {
142
- const bearerMatch = authHeader.match(/^Bearer\s+(.+)$/i);
143
- if (bearerMatch)
144
- return bearerMatch[1];
145
- }
146
- const query = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
147
- const queryAuth = query._auth;
148
- if (typeof queryAuth === "string")
149
- return queryAuth;
150
- const bodyAuth = parsedBody?._auth;
151
- if (typeof bodyAuth === "string")
152
- return bodyAuth;
153
- return "";
154
- }
155
- function authTokensMatch(submitted, authToken) {
156
- if (!authToken || !submitted)
157
- return false;
158
- const submittedBuffer = Buffer.from(submitted);
159
- const authTokenBuffer = Buffer.from(authToken);
160
- if (submittedBuffer.length !== authTokenBuffer.length)
161
- return false;
162
- return timingSafeEqual(submittedBuffer, authTokenBuffer);
163
- }
164
- function rejectUnauthorized(res, json = false) {
165
- if (json) {
166
- res.writeHead(401, { "content-type": "application/json" });
167
- res.end(JSON.stringify({ ok: false, error: "Unauthorized" }));
168
- return;
169
- }
170
- res.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
171
- res.end("Unauthorized");
172
- }
173
- function requireGetAuth(req, res, url, authToken, json = false) {
174
- if (!authToken)
175
- return true;
176
- const submitted = getSubmittedAuthToken(req, url);
177
- if (authTokensMatch(submitted, authToken))
178
- return true;
179
- rejectUnauthorized(res, json);
180
- return false;
181
- }
182
- function readFormBody(req, res) {
183
- const contentLength = parseInt(req.headers["content-length"] || "0", 10);
184
- if (contentLength > MAX_FORM_BODY_BYTES) {
185
- res.writeHead(413, { "content-type": "text/plain" });
186
- res.end("Request body too large");
187
- return Promise.resolve(null);
188
- }
189
- return new Promise((resolve) => {
190
- let body = "";
191
- let received = 0;
192
- req.on("data", (chunk) => {
193
- received += chunk.length;
194
- if (received > MAX_FORM_BODY_BYTES) {
195
- res.writeHead(413, { "Content-Type": "application/json" });
196
- res.end(JSON.stringify({ ok: false, error: "Request body too large" }));
197
- req.destroy();
198
- resolve(null);
199
- return;
200
- }
201
- body += String(chunk);
202
- });
203
- req.on("end", () => resolve(querystring.parse(body)));
204
- req.on("error", () => resolve(null));
205
- req.on("close", () => {
206
- if (received > MAX_FORM_BODY_BYTES)
207
- resolve(null);
208
- });
209
- });
210
- }
211
- function requirePostAuth(req, res, url, parsed, authToken, json = false) {
212
- if (!authToken)
213
- return true;
214
- const submitted = getSubmittedAuthToken(req, url, parsed);
215
- if (authTokensMatch(submitted, authToken))
216
- return true;
217
- rejectUnauthorized(res, json);
218
- return false;
219
- }
220
- function requireCsrf(res, parsed, csrfTokens, json = false) {
221
- if (!csrfTokens)
222
- return true;
223
- pruneExpiredCsrfTokens(csrfTokens);
224
- const submitted = String(parsed._csrf || "");
225
- if (submitted && csrfTokens.delete(submitted))
226
- return true;
227
- if (json) {
228
- res.writeHead(403, { "content-type": "application/json" });
229
- res.end(JSON.stringify({ ok: false, error: "Invalid or missing CSRF token" }));
230
- return false;
231
- }
232
- res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
233
- res.end("Invalid or missing CSRF token");
234
- return false;
235
- }
236
- function readProjectQueue(phrenPath, profile) {
237
- const projects = getProjectDirs(phrenPath, profile).map((projectDir) => path.basename(projectDir)).filter((project) => project !== "global");
238
- const items = [];
239
- for (const project of projects) {
240
- const queueResult = readReviewQueue(phrenPath, project);
241
- const queueItems = queueResult.ok ? queueResult.data : [];
242
- for (const item of queueItems) {
243
- items.push({
244
- project,
245
- section: item.section,
246
- line: item.line,
247
- text: item.text,
248
- date: item.date,
249
- machine: item.machine || undefined,
250
- model: item.model || undefined,
251
- });
252
- }
253
- }
254
- return items;
255
- }
256
- function parseTopicsPayload(raw) {
257
- try {
258
- const parsed = JSON.parse(raw);
259
- if (!Array.isArray(parsed))
260
- return null;
261
- return parsed.map((topic) => ({
262
- slug: String(topic?.slug || ""),
263
- label: String(topic?.label || ""),
264
- description: String(topic?.description || ""),
265
- keywords: Array.isArray(topic?.keywords) ? topic.keywords.map((keyword) => String(keyword)) : [],
266
- }));
267
- }
268
- catch {
269
- return null;
270
- }
271
- }
272
- function parseTopicPayload(raw) {
273
- try {
274
- const topic = JSON.parse(raw);
275
- if (!topic || typeof topic !== "object")
276
- return null;
277
- return {
278
- slug: String(topic.slug || ""),
279
- label: String(topic.label || ""),
280
- description: String(topic.description || ""),
281
- keywords: Array.isArray(topic.keywords)
282
- ? topic.keywords.map((keyword) => String(keyword))
283
- : [],
284
- };
285
- }
286
- catch {
287
- return null;
288
- }
289
- }
290
- export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
291
- try {
292
- repairPreexistingInstall(phrenPath);
293
- }
294
- catch (err) {
295
- if ((process.env.PHREN_DEBUG))
296
- process.stderr.write(`[phren] web-ui repair: ${errorMessage(err)}\n`);
297
- }
298
- const authToken = opts?.authToken;
299
- const csrfTokens = opts?.csrfTokens;
300
- return http.createServer(async (req, res) => {
301
- const url = req.url || "/";
302
- const pathname = url.includes("?") ? url.slice(0, url.indexOf("?")) : url;
303
- if (req.method === "GET" && pathname === "/") {
304
- if (!requireGetAuth(req, res, url, authToken))
305
- return;
306
- pruneExpiredCsrfTokens(csrfTokens);
307
- if (csrfTokens)
308
- csrfTokens.set(crypto.randomUUID(), Date.now());
309
- const nonce = crypto.randomBytes(16).toString("base64");
310
- setCommonHeaders(res, nonce);
311
- const html = renderPage(phrenPath, authToken, nonce);
312
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
313
- res.end(html);
314
- return;
315
- }
316
- setCommonHeaders(res);
317
- if (pathname.startsWith("/api/") && req.method === "GET" && !requireGetAuth(req, res, url, authToken)) {
318
- return;
319
- }
320
- if (req.method === "GET" && pathname === "/api/projects") {
321
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
322
- res.end(JSON.stringify(collectProjectsForUI(phrenPath, profile)));
323
- return;
324
- }
325
- if (req.method === "GET" && pathname === "/api/change-token") {
326
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
327
- res.end(JSON.stringify({ token: computePhrenLiveStateToken(phrenPath) }));
328
- return;
329
- }
330
- if (req.method === "GET" && pathname === "/api/runtime-health") {
331
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
332
- res.end(JSON.stringify(readSyncSnapshot(phrenPath)));
333
- return;
334
- }
335
- if (req.method === "POST" && pathname === "/api/sync") {
336
- void readFormBody(req, res).then((parsed) => {
337
- if (!parsed)
338
- return;
339
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
340
- return;
341
- if (!requireCsrf(res, parsed, csrfTokens, true))
342
- return;
343
- const message = String(parsed.message || "update phren");
344
- try {
345
- const EXEC_TIMEOUT = 15_000;
346
- const runGit = (args) => execFileSync("git", args, { cwd: phrenPath, encoding: "utf8", timeout: EXEC_TIMEOUT }).trim();
347
- const status = runGit(["status", "--porcelain"]);
348
- if (!status) {
349
- res.writeHead(200, { "content-type": "application/json" });
350
- res.end(JSON.stringify({ ok: true, message: "Nothing to sync — working tree clean." }));
351
- return;
352
- }
353
- runGit(["add", "--", "*.md", "*.json", "*.yaml", "*.yml", "*.jsonl", "*.txt"]);
354
- runGit(["commit", "-m", message]);
355
- let pushed = false;
356
- try {
357
- const remotes = runGit(["remote"]);
358
- if (remotes) {
359
- runGit(["push"]);
360
- pushed = true;
361
- }
362
- }
363
- catch { /* no remote or push failed */ }
364
- const changedFiles = status.split("\n").filter(Boolean).length;
365
- res.writeHead(200, { "content-type": "application/json" });
366
- res.end(JSON.stringify({ ok: true, message: `Synced ${changedFiles} file(s).${pushed ? " Pushed to remote." : " No remote, saved locally."}` }));
367
- }
368
- catch (err) {
369
- res.writeHead(200, { "content-type": "application/json" });
370
- res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
371
- }
372
- });
373
- return;
374
- }
375
- if (req.method === "GET" && pathname === "/api/review-queue") {
376
- if (!requireGetAuth(req, res, url, authToken, true))
377
- return;
378
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
379
- res.end(JSON.stringify(readProjectQueue(phrenPath, profile)));
380
- return;
381
- }
382
- if (req.method === "GET" && pathname === "/api/review-activity") {
383
- if (!requireGetAuth(req, res, url, authToken, true))
384
- return;
385
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
386
- res.end(JSON.stringify({
387
- accepted: recentAccepted(phrenPath),
388
- usage: recentUsage(phrenPath),
389
- }));
390
- return;
391
- }
392
- // POST /api/approve — remove item from review queue (keep finding)
393
- if (req.method === "POST" && pathname === "/api/approve") {
394
- void readFormBody(req, res).then((parsed) => {
395
- if (!parsed)
396
- return;
397
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
398
- return;
399
- if (!requireCsrf(res, parsed, csrfTokens, true))
400
- return;
401
- const project = String(parsed.project || "");
402
- const line = String(parsed.line || "");
403
- if (!project || !isValidProjectName(project) || !line) {
404
- res.writeHead(200, { "content-type": "application/json" });
405
- res.end(JSON.stringify({ ok: false, error: "Missing project or line" }));
406
- return;
407
- }
408
- try {
409
- const qPath = queueFilePath(phrenPath, project);
410
- if (fs.existsSync(qPath)) {
411
- const content = fs.readFileSync(qPath, "utf8");
412
- const lines = content.split("\n");
413
- const filtered = lines.filter((l) => l.trim() !== line.trim());
414
- fs.writeFileSync(qPath, filtered.join("\n"));
415
- }
416
- res.writeHead(200, { "content-type": "application/json" });
417
- res.end(JSON.stringify({ ok: true }));
418
- }
419
- catch (err) {
420
- res.writeHead(200, { "content-type": "application/json" });
421
- res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
422
- }
423
- });
424
- return;
425
- }
426
- // POST /api/reject — remove item from review queue AND remove finding
427
- if (req.method === "POST" && pathname === "/api/reject") {
428
- void readFormBody(req, res).then((parsed) => {
429
- if (!parsed)
430
- return;
431
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
432
- return;
433
- if (!requireCsrf(res, parsed, csrfTokens, true))
434
- return;
435
- const project = String(parsed.project || "");
436
- const line = String(parsed.line || "");
437
- if (!project || !isValidProjectName(project) || !line) {
438
- res.writeHead(200, { "content-type": "application/json" });
439
- res.end(JSON.stringify({ ok: false, error: "Missing project or line" }));
440
- return;
441
- }
442
- try {
443
- // Remove from review queue
444
- const qPath = queueFilePath(phrenPath, project);
445
- if (fs.existsSync(qPath)) {
446
- const content = fs.readFileSync(qPath, "utf8");
447
- const lines = content.split("\n");
448
- const filtered = lines.filter((l) => l.trim() !== line.trim());
449
- fs.writeFileSync(qPath, filtered.join("\n"));
450
- }
451
- // Also remove the finding from FINDINGS.md
452
- // Extract text from the line (strip "- " prefix and inline metadata)
453
- const findingText = line.replace(/^-\s*/, "").replace(/<!--.*?-->/g, "").trim();
454
- if (findingText) {
455
- removeFinding(phrenPath, project, findingText);
456
- }
457
- res.writeHead(200, { "content-type": "application/json" });
458
- res.end(JSON.stringify({ ok: true }));
459
- }
460
- catch (err) {
461
- res.writeHead(200, { "content-type": "application/json" });
462
- res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
463
- }
464
- });
465
- return;
466
- }
467
- // POST /api/edit — edit a finding's text
468
- if (req.method === "POST" && pathname === "/api/edit") {
469
- void readFormBody(req, res).then((parsed) => {
470
- if (!parsed)
471
- return;
472
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
473
- return;
474
- if (!requireCsrf(res, parsed, csrfTokens, true))
475
- return;
476
- const project = String(parsed.project || "");
477
- const line = String(parsed.line || "");
478
- const newText = String(parsed.new_text || "");
479
- if (!project || !isValidProjectName(project) || !line || !newText) {
480
- res.writeHead(200, { "content-type": "application/json" });
481
- res.end(JSON.stringify({ ok: false, error: "Missing project, line, or new_text" }));
482
- return;
483
- }
484
- try {
485
- const oldText = line.replace(/^-\s*/, "").replace(/<!--.*?-->/g, "").trim();
486
- const result = editFinding(phrenPath, project, oldText, newText);
487
- res.writeHead(200, { "content-type": "application/json" });
488
- res.end(JSON.stringify({ ok: result.ok, error: result.ok ? undefined : result.error }));
489
- }
490
- catch (err) {
491
- res.writeHead(200, { "content-type": "application/json" });
492
- res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
493
- }
494
- });
495
- return;
496
- }
497
- if (req.method === "GET" && pathname.startsWith("/api/project-content")) {
498
- const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
499
- const project = String(qs.project || "");
500
- const file = String(qs.file || "");
501
- if (!project || !isValidProjectName(project) || !file) {
502
- res.writeHead(400, { "content-type": "application/json" });
503
- res.end(JSON.stringify({ ok: false, error: "Invalid project or file" }));
504
- return;
505
- }
506
- const allowedFiles = ["FINDINGS.md", TASKS_FILENAME, "CLAUDE.md", "summary.md"];
507
- if (!allowedFiles.includes(file)) {
508
- res.writeHead(400, { "content-type": "application/json" });
509
- res.end(JSON.stringify({ ok: false, error: `File not allowed: ${file}` }));
510
- return;
511
- }
512
- const filePath = safeProjectPath(phrenPath, project, file);
513
- if (!filePath) {
514
- res.writeHead(400, { "content-type": "application/json" });
515
- res.end(JSON.stringify({ ok: false, error: "Invalid project or file path" }));
516
- return;
517
- }
518
- if (!fs.existsSync(filePath)) {
519
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
520
- res.end(JSON.stringify({ ok: false, error: `File not found: ${file}` }));
521
- return;
522
- }
523
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
524
- res.end(JSON.stringify({ ok: true, content: fs.readFileSync(filePath, "utf8") }));
525
- return;
526
- }
527
- if (req.method === "GET" && pathname === "/api/project-topics") {
528
- const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
529
- const project = String(qs.project || "");
530
- if (!project || !isValidProjectName(project)) {
531
- res.writeHead(400, { "content-type": "application/json" });
532
- res.end(JSON.stringify({ ok: false, error: "Invalid project" }));
533
- return;
534
- }
535
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
536
- res.end(JSON.stringify({ ok: true, ...getProjectTopicsResponse(phrenPath, project) }));
537
- return;
538
- }
539
- if (req.method === "GET" && pathname === "/api/project-reference-list") {
540
- const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
541
- const project = String(qs.project || "");
542
- if (!project || !isValidProjectName(project)) {
543
- res.writeHead(400, { "content-type": "application/json" });
544
- res.end(JSON.stringify({ ok: false, error: "Invalid project" }));
545
- return;
546
- }
547
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
548
- res.end(JSON.stringify({ ok: true, ...listProjectReferenceDocs(phrenPath, project) }));
549
- return;
550
- }
551
- if (req.method === "GET" && pathname === "/api/project-reference-content") {
552
- const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
553
- const project = String(qs.project || "");
554
- const file = String(qs.file || "");
555
- const contentResult = readReferenceContent(phrenPath, project, file);
556
- res.writeHead(contentResult.ok ? 200 : 400, { "content-type": "application/json; charset=utf-8" });
557
- res.end(JSON.stringify(contentResult.ok ? { ok: true, content: contentResult.content } : { ok: false, error: contentResult.error }));
558
- return;
559
- }
560
- if (req.method === "GET" && pathname === "/api/skills") {
561
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
562
- res.end(JSON.stringify(collectSkillsForUI(phrenPath, profile)));
563
- return;
564
- }
565
- if (req.method === "GET" && pathname.startsWith("/api/skill-content")) {
566
- const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
567
- const filePath = String(qs.path || "");
568
- if (!filePath || !isAllowedFilePath(filePath, phrenPath)) {
569
- res.writeHead(400, { "content-type": "application/json" });
570
- res.end(JSON.stringify({ ok: false, error: "Invalid path" }));
571
- return;
572
- }
573
- if (!fs.existsSync(filePath)) {
574
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
575
- res.end(JSON.stringify({ ok: false, error: "File not found" }));
576
- return;
577
- }
578
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
579
- res.end(JSON.stringify({ ok: true, content: fs.readFileSync(filePath, "utf8") }));
580
- return;
581
- }
582
- if (req.method === "GET" && pathname === "/api/hooks") {
583
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
584
- res.end(JSON.stringify(getHooksData(phrenPath)));
585
- return;
586
- }
587
- if (req.method === "POST" && pathname === "/api/skill-save") {
588
- void readFormBody(req, res).then((parsed) => {
589
- if (!parsed)
590
- return;
591
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
592
- return;
593
- if (!requireCsrf(res, parsed, csrfTokens, true))
594
- return;
595
- const filePath = String(parsed.path || "");
596
- const content = String(parsed.content || "");
597
- if (!filePath || !isAllowedFilePath(filePath, phrenPath)) {
598
- res.writeHead(200, { "content-type": "application/json" });
599
- res.end(JSON.stringify({ ok: false, error: "Invalid path" }));
600
- return;
601
- }
602
- try {
603
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
604
- const tmpPath = `${filePath}.tmp-${crypto.randomUUID()}`;
605
- fs.writeFileSync(tmpPath, content);
606
- fs.renameSync(tmpPath, filePath);
607
- res.writeHead(200, { "content-type": "application/json" });
608
- res.end(JSON.stringify({ ok: true }));
609
- }
610
- catch (err) {
611
- res.writeHead(200, { "content-type": "application/json" });
612
- res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
613
- }
614
- });
615
- return;
616
- }
617
- if (req.method === "POST" && pathname === "/api/skill-toggle") {
618
- void readFormBody(req, res).then((parsed) => {
619
- if (!parsed)
620
- return;
621
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
622
- return;
623
- if (!requireCsrf(res, parsed, csrfTokens, true))
624
- return;
625
- const project = String(parsed.project || "");
626
- const name = String(parsed.name || "");
627
- const enabled = String(parsed.enabled || "") === "true";
628
- if (!project || !name || (project.toLowerCase() !== "global" && !isValidProjectName(project))) {
629
- res.writeHead(200, { "content-type": "application/json" });
630
- res.end(JSON.stringify({ ok: false, error: "Invalid skill toggle request" }));
631
- return;
632
- }
633
- const skill = findSkill(phrenPath, profile || "", project, name);
634
- if (!skill || "error" in skill) {
635
- res.writeHead(200, { "content-type": "application/json" });
636
- res.end(JSON.stringify({ ok: false, error: skill && "error" in skill ? skill.error : "Skill not found" }));
637
- return;
638
- }
639
- setSkillEnabledAndSync(phrenPath, project, skill.name, enabled);
640
- res.writeHead(200, { "content-type": "application/json" });
641
- res.end(JSON.stringify({ ok: true, enabled }));
642
- });
643
- return;
644
- }
645
- if (req.method === "POST" && pathname === "/api/hook-toggle") {
646
- void readFormBody(req, res).then((parsed) => {
647
- if (!parsed)
648
- return;
649
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
650
- return;
651
- if (!requireCsrf(res, parsed, csrfTokens, true))
652
- return;
653
- const tool = String(parsed.tool || "").toLowerCase();
654
- const validTools = ["claude", "copilot", "cursor", "codex"];
655
- if (!validTools.includes(tool)) {
656
- res.writeHead(200, { "content-type": "application/json" });
657
- res.end(JSON.stringify({ ok: false, error: "Invalid tool" }));
658
- return;
659
- }
660
- const prefs = readInstallPreferences(phrenPath);
661
- const toolPrefs = (prefs.hookTools && typeof prefs.hookTools === "object") ? prefs.hookTools : {};
662
- const current = toolPrefs[tool] !== false && prefs.hooksEnabled !== false;
663
- writeInstallPreferences(phrenPath, {
664
- hookTools: { ...toolPrefs, [tool]: !current },
665
- });
666
- res.writeHead(200, { "content-type": "application/json" });
667
- res.end(JSON.stringify({ ok: true, enabled: !current }));
668
- });
669
- return;
670
- }
671
- if (req.method === "POST" && pathname === "/api/project-topics/save") {
672
- void readFormBody(req, res).then((parsed) => {
673
- if (!parsed)
674
- return;
675
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
676
- return;
677
- if (!requireCsrf(res, parsed, csrfTokens, true))
678
- return;
679
- const project = String(parsed.project || "");
680
- const rawTopics = String(parsed.topics || "");
681
- if (!project || !isValidProjectName(project)) {
682
- res.writeHead(400, { "content-type": "application/json" });
683
- res.end(JSON.stringify({ ok: false, error: "Invalid project" }));
684
- return;
685
- }
686
- const topics = parseTopicsPayload(rawTopics);
687
- if (!topics) {
688
- res.writeHead(400, { "content-type": "application/json" });
689
- res.end(JSON.stringify({ ok: false, error: "Invalid topics payload" }));
690
- return;
691
- }
692
- const saved = writeProjectTopics(phrenPath, project, topics);
693
- if (!saved.ok) {
694
- res.writeHead(200, { "content-type": "application/json" });
695
- res.end(JSON.stringify(saved));
696
- return;
697
- }
698
- for (const topic of saved.topics) {
699
- const ensured = ensureTopicReferenceDoc(phrenPath, project, topic);
700
- if (!ensured.ok) {
701
- res.writeHead(200, { "content-type": "application/json" });
702
- res.end(JSON.stringify({ ok: false, error: ensured.error }));
703
- return;
704
- }
705
- }
706
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
707
- res.end(JSON.stringify({ ok: true, ...getProjectTopicsResponse(phrenPath, project) }));
708
- });
709
- return;
710
- }
711
- if (req.method === "POST" && pathname === "/api/project-topics/reclassify") {
712
- void readFormBody(req, res).then((parsed) => {
713
- if (!parsed)
714
- return;
715
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
716
- return;
717
- if (!requireCsrf(res, parsed, csrfTokens, true))
718
- return;
719
- const project = String(parsed.project || "");
720
- if (!project || !isValidProjectName(project)) {
721
- res.writeHead(400, { "content-type": "application/json" });
722
- res.end(JSON.stringify({ ok: false, error: "Invalid project" }));
723
- return;
724
- }
725
- const result = reclassifyLegacyTopicDocs(phrenPath, project);
726
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
727
- res.end(JSON.stringify({ ok: true, ...result }));
728
- });
729
- return;
730
- }
731
- if (req.method === "POST" && pathname === "/api/project-topics/pin") {
732
- void readFormBody(req, res).then((parsed) => {
733
- if (!parsed)
734
- return;
735
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
736
- return;
737
- if (!requireCsrf(res, parsed, csrfTokens, true))
738
- return;
739
- const project = String(parsed.project || "");
740
- const rawTopic = String(parsed.topic || "");
741
- if (!project || !isValidProjectName(project)) {
742
- res.writeHead(400, { "content-type": "application/json" });
743
- res.end(JSON.stringify({ ok: false, error: "Invalid project" }));
744
- return;
745
- }
746
- const topic = parseTopicPayload(rawTopic);
747
- if (!topic) {
748
- res.writeHead(400, { "content-type": "application/json" });
749
- res.end(JSON.stringify({ ok: false, error: "Invalid topic payload" }));
750
- return;
751
- }
752
- const pinned = pinProjectTopicSuggestion(phrenPath, project, topic);
753
- if (!pinned.ok) {
754
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
755
- res.end(JSON.stringify(pinned));
756
- return;
757
- }
758
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
759
- res.end(JSON.stringify({ ok: true, ...getProjectTopicsResponse(phrenPath, project) }));
760
- });
761
- return;
762
- }
763
- if (req.method === "POST" && pathname === "/api/project-topics/unpin") {
764
- void readFormBody(req, res).then((parsed) => {
765
- if (!parsed)
766
- return;
767
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
768
- return;
769
- if (!requireCsrf(res, parsed, csrfTokens, true))
770
- return;
771
- const project = String(parsed.project || "");
772
- const slug = String(parsed.slug || "");
773
- if (!project || !isValidProjectName(project)) {
774
- res.writeHead(400, { "content-type": "application/json" });
775
- res.end(JSON.stringify({ ok: false, error: "Invalid project" }));
776
- return;
777
- }
778
- const unpinned = unpinProjectTopicSuggestion(phrenPath, project, slug);
779
- if (!unpinned.ok) {
780
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
781
- res.end(JSON.stringify(unpinned));
782
- return;
783
- }
784
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
785
- res.end(JSON.stringify({ ok: true, ...getProjectTopicsResponse(phrenPath, project) }));
786
- });
787
- return;
788
- }
789
- if (req.method === "GET" && pathname === "/api/search") {
790
- if (!requireGetAuth(req, res, url, authToken, true))
791
- return;
792
- const searchParams = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
793
- const query = searchParams.get("q") || searchParams.get("query") || "";
794
- const searchProject = searchParams.get("project") || undefined;
795
- const searchType = searchParams.get("type") || undefined;
796
- const searchLimit = parseInt(searchParams.get("limit") || "10", 10) || 10;
797
- if (!query.trim()) {
798
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
799
- res.end(JSON.stringify({ ok: false, error: "Missing query parameter (q or query)." }));
800
- return;
801
- }
802
- try {
803
- const { runSearch } = await import("./cli-search.js");
804
- const result = await runSearch({ query, limit: Math.min(searchLimit, 50), project: searchProject, type: searchType }, phrenPath, profile || "");
805
- // Build file date map from source headers like [project/filename]
806
- const fileDates = {};
807
- for (const line of result.lines) {
808
- const srcMatch = line.match(/^\[([^\]]+)\]\s/);
809
- if (srcMatch) {
810
- const sourceKey = srcMatch[1];
811
- if (fileDates[sourceKey])
812
- continue;
813
- const slashIdx = sourceKey.indexOf("/");
814
- if (slashIdx > 0) {
815
- const proj = sourceKey.slice(0, slashIdx);
816
- const file = sourceKey.slice(slashIdx + 1);
817
- try {
818
- const filePath = path.join(phrenPath, proj, file);
819
- if (fs.existsSync(filePath)) {
820
- const stat = fs.statSync(filePath);
821
- fileDates[sourceKey] = stat.mtime.toISOString();
822
- }
823
- }
824
- catch { /* skip */ }
825
- }
826
- }
827
- }
828
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
829
- res.end(JSON.stringify({ ok: true, query, results: result.lines, fileDates }));
830
- }
831
- catch (err) {
832
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
833
- res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
834
- }
835
- return;
836
- }
837
- if (req.method === "GET" && pathname.startsWith("/api/graph")) {
838
- const graphParams = new URLSearchParams(url.includes("?") ? url.slice(url.indexOf("?") + 1) : "");
839
- const focusProject = graphParams.get("project") || undefined;
840
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
841
- res.end(JSON.stringify(await buildGraph(phrenPath, profile, focusProject)));
842
- return;
843
- }
844
- if (req.method === "GET" && pathname === "/api/scores") {
845
- let scores = {};
846
- try {
847
- const raw = fs.readFileSync(path.join(phrenPath, ".runtime", "memory-scores.json"), "utf-8");
848
- const parsed = JSON.parse(raw);
849
- if (parsed && typeof parsed === "object") {
850
- scores = parsed;
851
- }
852
- }
853
- catch {
854
- // file missing or unparseable – return empty
855
- }
856
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
857
- res.end(JSON.stringify(scores));
858
- return;
859
- }
860
- if (req.method === "GET" && pathname === "/api/tasks") {
861
- if (!requireGetAuth(req, res, url, authToken, true))
862
- return;
863
- try {
864
- const docs = readTasksAcrossProjects(phrenPath, profile);
865
- const tasks = [];
866
- for (const doc of docs) {
867
- for (const section of ["Active", "Queue", "Done"]) {
868
- for (const item of doc.items[section]) {
869
- tasks.push({
870
- project: doc.project,
871
- section: item.section,
872
- line: item.line,
873
- priority: item.priority,
874
- pinned: item.pinned,
875
- githubIssue: item.githubIssue,
876
- githubUrl: item.githubUrl,
877
- context: item.context,
878
- checked: item.checked,
879
- sessionId: item.sessionId,
880
- });
881
- }
882
- }
883
- }
884
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
885
- res.end(JSON.stringify({ ok: true, tasks }));
886
- }
887
- catch (err) {
888
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
889
- res.end(JSON.stringify({ ok: false, error: errorMessage(err), tasks: [] }));
890
- }
891
- return;
892
- }
893
- if (req.method === "POST" && pathname === "/api/tasks/complete") {
894
- void readFormBody(req, res).then((parsed) => {
895
- if (!parsed)
896
- return;
897
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
898
- return;
899
- if (!requireCsrf(res, parsed, csrfTokens, true))
900
- return;
901
- const project = String(parsed.project || "");
902
- const item = String(parsed.item || "");
903
- if (!project || !item || !isValidProjectName(project)) {
904
- res.writeHead(400, { "content-type": "application/json" });
905
- res.end(JSON.stringify({ ok: false, error: "Missing or invalid project/item" }));
906
- return;
907
- }
908
- const result = completeTaskStore(phrenPath, project, item);
909
- res.writeHead(200, { "content-type": "application/json" });
910
- res.end(JSON.stringify({ ok: result.ok, message: result.ok ? result.data : undefined, error: result.ok ? undefined : result.error }));
911
- });
912
- return;
913
- }
914
- if (req.method === "POST" && pathname === "/api/tasks/add") {
915
- void readFormBody(req, res).then((parsed) => {
916
- if (!parsed)
917
- return;
918
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
919
- return;
920
- if (!requireCsrf(res, parsed, csrfTokens, true))
921
- return;
922
- const project = String(parsed.project || "");
923
- const item = String(parsed.item || "");
924
- if (!project || !item || !isValidProjectName(project)) {
925
- res.writeHead(400, { "content-type": "application/json" });
926
- res.end(JSON.stringify({ ok: false, error: "Missing or invalid project/item" }));
927
- return;
928
- }
929
- const result = addTaskStore(phrenPath, project, item);
930
- res.writeHead(200, { "content-type": "application/json" });
931
- res.end(JSON.stringify({ ok: result.ok, message: result.ok ? `Task added: ${result.data.line}` : undefined, error: result.ok ? undefined : result.error }));
932
- });
933
- return;
934
- }
935
- if (req.method === "POST" && pathname === "/api/tasks/remove") {
936
- void readFormBody(req, res).then((parsed) => {
937
- if (!parsed)
938
- return;
939
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
940
- return;
941
- if (!requireCsrf(res, parsed, csrfTokens, true))
942
- return;
943
- const project = String(parsed.project || "");
944
- const item = String(parsed.item || "");
945
- if (!project || !item || !isValidProjectName(project)) {
946
- res.writeHead(400, { "content-type": "application/json" });
947
- res.end(JSON.stringify({ ok: false, error: "Missing or invalid project/item" }));
948
- return;
949
- }
950
- const result = removeTaskStore(phrenPath, project, item);
951
- res.writeHead(200, { "content-type": "application/json" });
952
- res.end(JSON.stringify({ ok: result.ok, message: result.ok ? result.data : undefined, error: result.ok ? undefined : result.error }));
953
- });
954
- return;
955
- }
956
- if (req.method === "POST" && pathname === "/api/tasks/update") {
957
- void readFormBody(req, res).then((parsed) => {
958
- if (!parsed)
959
- return;
960
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
961
- return;
962
- if (!requireCsrf(res, parsed, csrfTokens, true))
963
- return;
964
- const project = String(parsed.project || "");
965
- const item = String(parsed.item || "");
966
- if (!project || !item || !isValidProjectName(project)) {
967
- res.writeHead(400, { "content-type": "application/json" });
968
- res.end(JSON.stringify({ ok: false, error: "Missing or invalid project/item" }));
969
- return;
970
- }
971
- const updates = {};
972
- if (Object.prototype.hasOwnProperty.call(parsed, "text"))
973
- updates.text = String(parsed.text || "");
974
- if (Object.prototype.hasOwnProperty.call(parsed, "priority"))
975
- updates.priority = String(parsed.priority || "");
976
- if (Object.prototype.hasOwnProperty.call(parsed, "section"))
977
- updates.section = String(parsed.section || "");
978
- const result = updateTaskStore(phrenPath, project, item, updates);
979
- res.writeHead(200, { "content-type": "application/json" });
980
- res.end(JSON.stringify({ ok: result.ok, message: result.ok ? result.data : undefined, error: result.ok ? undefined : result.error }));
981
- });
982
- return;
983
- }
984
- if (req.method === "GET" && pathname === "/api/settings") {
985
- if (!requireGetAuth(req, res, url, authToken, true))
986
- return;
987
- try {
988
- const prefs = readInstallPreferences(phrenPath);
989
- const workflowPolicy = getWorkflowPolicy(phrenPath);
990
- const retentionPolicy = getRetentionPolicy(phrenPath);
991
- const hooksData = getHooksData(phrenPath);
992
- const proactivityFindings = prefs.proactivityFindings || prefs.proactivity || "high";
993
- const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
994
- const settingsProject = String(qs.project || "");
995
- const merged = settingsProject && isValidProjectName(settingsProject) ? mergeConfig(phrenPath, settingsProject) : null;
996
- const overrides = settingsProject && isValidProjectName(settingsProject) ? getProjectConfigOverrides(phrenPath, settingsProject) : null;
997
- // Build project info when a specific project is selected
998
- let projectInfo = null;
999
- if (settingsProject && isValidProjectName(settingsProject)) {
1000
- const projectDir = path.join(phrenPath, settingsProject);
1001
- const configFile = path.join(projectDir, "phren.project.yaml");
1002
- const projConfig = readProjectConfig(phrenPath, settingsProject);
1003
- const findingsPath = path.join(projectDir, "FINDINGS.md");
1004
- const taskPath = path.join(projectDir, "tasks.md");
1005
- let findingCount = 0;
1006
- if (fs.existsSync(findingsPath)) {
1007
- findingCount = (fs.readFileSync(findingsPath, "utf8").match(/^- /gm) || []).length;
1008
- }
1009
- let taskCount = 0;
1010
- if (fs.existsSync(taskPath)) {
1011
- const queueMatch = fs.readFileSync(taskPath, "utf8").match(/## Queue[\s\S]*?(?=## |$)/);
1012
- if (queueMatch)
1013
- taskCount = (queueMatch[0].match(/^- /gm) || []).length;
1014
- }
1015
- projectInfo = {
1016
- diskPath: projConfig.sourcePath || projectDir,
1017
- ownership: projConfig.ownership || "default",
1018
- configFile,
1019
- configExists: fs.existsSync(configFile),
1020
- hasFindings: fs.existsSync(findingsPath),
1021
- hasTasks: fs.existsSync(taskPath),
1022
- hasSummary: fs.existsSync(path.join(projectDir, "summary.md")),
1023
- hasClaudeMd: fs.existsSync(path.join(projectDir, "CLAUDE.md")),
1024
- findingCount,
1025
- taskCount,
1026
- };
1027
- }
1028
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1029
- res.end(JSON.stringify({
1030
- ok: true,
1031
- proactivity: prefs.proactivity || "high",
1032
- proactivityFindings,
1033
- proactivityTask: prefs.proactivityTask || prefs.proactivity || "high",
1034
- taskMode: workflowPolicy.taskMode,
1035
- findingSensitivity: workflowPolicy.findingSensitivity || "balanced",
1036
- autoCaptureEnabled: proactivityFindings !== "low",
1037
- consolidationEntryThreshold: CONSOLIDATION_ENTRY_THRESHOLD,
1038
- hooksEnabled: hooksData.globalEnabled,
1039
- mcpEnabled: prefs.mcpEnabled !== false,
1040
- hookTools: hooksData.tools,
1041
- retentionPolicy,
1042
- workflowPolicy,
1043
- merged,
1044
- overrides,
1045
- projectInfo,
1046
- }));
1047
- }
1048
- catch (err) {
1049
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1050
- res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
1051
- }
1052
- return;
1053
- }
1054
- if (req.method === "POST" && pathname === "/api/settings/finding-sensitivity") {
1055
- void readFormBody(req, res).then((parsed) => {
1056
- if (!parsed)
1057
- return;
1058
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
1059
- return;
1060
- if (!requireCsrf(res, parsed, csrfTokens, true))
1061
- return;
1062
- const value = String(parsed.value || "");
1063
- const valid = ["minimal", "conservative", "balanced", "aggressive"];
1064
- if (!valid.includes(value)) {
1065
- res.writeHead(200, { "content-type": "application/json" });
1066
- res.end(JSON.stringify({ ok: false, error: `Invalid finding sensitivity: "${value}". Must be one of: ${valid.join(", ")}` }));
1067
- return;
1068
- }
1069
- const result = updateWorkflowPolicy(phrenPath, { findingSensitivity: value });
1070
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1071
- res.end(JSON.stringify(result.ok ? { ok: true, findingSensitivity: result.data.findingSensitivity } : { ok: false, error: result.error }));
1072
- });
1073
- return;
1074
- }
1075
- if (req.method === "POST" && pathname === "/api/settings/task-mode") {
1076
- void readFormBody(req, res).then((parsed) => {
1077
- if (!parsed)
1078
- return;
1079
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
1080
- return;
1081
- if (!requireCsrf(res, parsed, csrfTokens, true))
1082
- return;
1083
- const value = String(parsed.value || "").trim().toLowerCase();
1084
- const valid = VALID_TASK_MODES;
1085
- if (!valid.includes(value)) {
1086
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1087
- res.end(JSON.stringify({ ok: false, error: `Invalid task mode: "${value}". Must be one of: ${valid.join(", ")}` }));
1088
- return;
1089
- }
1090
- const result = updateWorkflowPolicy(phrenPath, { taskMode: value });
1091
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1092
- res.end(JSON.stringify(result.ok ? { ok: true, taskMode: result.data.taskMode } : { ok: false, error: result.error }));
1093
- });
1094
- return;
1095
- }
1096
- if (req.method === "POST" && pathname === "/api/settings/proactivity") {
1097
- void readFormBody(req, res).then((parsed) => {
1098
- if (!parsed)
1099
- return;
1100
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
1101
- return;
1102
- if (!requireCsrf(res, parsed, csrfTokens, true))
1103
- return;
1104
- const value = String(parsed.value || "").trim().toLowerCase();
1105
- const valid = ["high", "medium", "low"];
1106
- if (!valid.includes(value)) {
1107
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1108
- res.end(JSON.stringify({ ok: false, error: `Invalid proactivity: "${value}". Must be one of: ${valid.join(", ")}` }));
1109
- return;
1110
- }
1111
- writeInstallPreferences(phrenPath, { proactivity: value });
1112
- writeGovernanceInstallPreferences(phrenPath, { proactivity: value });
1113
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1114
- res.end(JSON.stringify({ ok: true, proactivity: value }));
1115
- });
1116
- return;
1117
- }
1118
- if (req.method === "POST" && pathname === "/api/settings/auto-capture") {
1119
- void readFormBody(req, res).then((parsed) => {
1120
- if (!parsed)
1121
- return;
1122
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
1123
- return;
1124
- if (!requireCsrf(res, parsed, csrfTokens, true))
1125
- return;
1126
- const enabled = String(parsed.enabled || "").toLowerCase() === "true";
1127
- const next = enabled ? "high" : "low";
1128
- writeInstallPreferences(phrenPath, { proactivityFindings: next });
1129
- writeGovernanceInstallPreferences(phrenPath, { proactivityFindings: next });
1130
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1131
- res.end(JSON.stringify({ ok: true, autoCaptureEnabled: enabled, proactivityFindings: next }));
1132
- });
1133
- return;
1134
- }
1135
- if (req.method === "POST" && pathname === "/api/settings/mcp-enabled") {
1136
- void readFormBody(req, res).then((parsed) => {
1137
- if (!parsed)
1138
- return;
1139
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
1140
- return;
1141
- if (!requireCsrf(res, parsed, csrfTokens, true))
1142
- return;
1143
- const enabled = String(parsed.enabled || "").toLowerCase() === "true";
1144
- writeInstallPreferences(phrenPath, { mcpEnabled: enabled });
1145
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1146
- res.end(JSON.stringify({ ok: true, mcpEnabled: enabled }));
1147
- });
1148
- return;
1149
- }
1150
- // POST /api/settings/project-overrides — write per-project config overrides
1151
- if (req.method === "POST" && pathname === "/api/settings/project-overrides") {
1152
- void readFormBody(req, res).then((parsed) => {
1153
- if (!parsed)
1154
- return;
1155
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
1156
- return;
1157
- if (!requireCsrf(res, parsed, csrfTokens, true))
1158
- return;
1159
- const project = String(parsed.project || "");
1160
- const field = String(parsed.field || "");
1161
- const value = String(parsed.value || "");
1162
- const clearField = String(parsed.clear || "") === "true";
1163
- if (!project || !isValidProjectName(project)) {
1164
- res.writeHead(400, { "content-type": "application/json" });
1165
- res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1166
- return;
1167
- }
1168
- const registeredProjects = getProjectDirs(phrenPath, profile).map((d) => path.basename(d)).filter((p) => p !== "global");
1169
- const isRegistered = registeredProjects.includes(project);
1170
- const registrationWarning = isRegistered ? undefined : `Project '${project}' is not registered in the active profile. Config was saved but it will have no effect until the project is added with 'phren add'.`;
1171
- const VALID_FIELDS = {
1172
- findingSensitivity: ["minimal", "conservative", "balanced", "aggressive"],
1173
- proactivity: ["high", "medium", "low"],
1174
- proactivityFindings: ["high", "medium", "low"],
1175
- proactivityTask: ["high", "medium", "low"],
1176
- taskMode: ["off", "manual", "suggest", "auto"],
1177
- };
1178
- const NUMERIC_RETENTION_FIELDS = ["ttlDays", "retentionDays", "autoAcceptThreshold", "minInjectConfidence"];
1179
- const NUMERIC_WORKFLOW_FIELDS = ["lowConfidenceThreshold"];
1180
- try {
1181
- updateProjectConfigOverrides(phrenPath, project, (current) => {
1182
- const next = { ...current };
1183
- if (clearField) {
1184
- if (field in VALID_FIELDS)
1185
- delete next[field];
1186
- else if (NUMERIC_RETENTION_FIELDS.includes(field)) {
1187
- if (next.retentionPolicy)
1188
- delete next.retentionPolicy[field];
1189
- }
1190
- else if (NUMERIC_WORKFLOW_FIELDS.includes(field)) {
1191
- if (next.workflowPolicy)
1192
- delete next.workflowPolicy[field];
1193
- }
1194
- return next;
1195
- }
1196
- if (field in VALID_FIELDS) {
1197
- const allowed = VALID_FIELDS[field];
1198
- if (!allowed.includes(value))
1199
- throw new Error(`Invalid value "${value}" for ${field}`);
1200
- next[field] = value;
1201
- }
1202
- else if (NUMERIC_RETENTION_FIELDS.includes(field)) {
1203
- const num = parseFloat(value);
1204
- if (!Number.isFinite(num) || num < 0)
1205
- throw new Error(`Invalid numeric value for ${field}`);
1206
- next.retentionPolicy = { ...next.retentionPolicy, [field]: num };
1207
- }
1208
- else if (NUMERIC_WORKFLOW_FIELDS.includes(field)) {
1209
- const num = parseFloat(value);
1210
- if (!Number.isFinite(num) || num < 0 || num > 1)
1211
- throw new Error(`Invalid value for ${field} (must be 0–1)`);
1212
- next.workflowPolicy = { ...next.workflowPolicy, [field]: num };
1213
- }
1214
- else {
1215
- throw new Error(`Unknown config field: ${field}`);
1216
- }
1217
- return next;
1218
- });
1219
- const merged = mergeConfig(phrenPath, project);
1220
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1221
- res.end(JSON.stringify({ ok: true, config: merged, ...(registrationWarning ? { warning: registrationWarning } : {}) }));
1222
- }
1223
- catch (err) {
1224
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1225
- res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
1226
- }
1227
- });
1228
- return;
1229
- }
1230
- if (req.method === "GET" && pathname === "/api/config") {
1231
- if (!requireGetAuth(req, res, url, authToken, true))
1232
- return;
1233
- const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
1234
- const project = String(qs.project || "");
1235
- if (project && !isValidProjectName(project)) {
1236
- res.writeHead(400, { "content-type": "application/json" });
1237
- res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1238
- return;
1239
- }
1240
- try {
1241
- const config = mergeConfig(phrenPath, project || undefined);
1242
- const projects = getProjectDirs(phrenPath, profile).map((d) => path.basename(d)).filter((p) => p !== "global");
1243
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1244
- res.end(JSON.stringify({ ok: true, config, projects }));
1245
- }
1246
- catch (err) {
1247
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1248
- res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
1249
- }
1250
- return;
1251
- }
1252
- if (req.method === "GET" && pathname === "/api/csrf-token") {
1253
- if (!requireGetAuth(req, res, url, authToken, true))
1254
- return;
1255
- if (!csrfTokens) {
1256
- res.writeHead(200, { "content-type": "application/json" });
1257
- res.end(JSON.stringify({ ok: true, token: null }));
1258
- return;
1259
- }
1260
- pruneExpiredCsrfTokens(csrfTokens);
1261
- const token = crypto.randomUUID();
1262
- csrfTokens.set(token, Date.now());
1263
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1264
- res.end(JSON.stringify({ ok: true, token }));
1265
- return;
1266
- }
1267
- // GET /api/findings/:project — list findings for a project
1268
- if (req.method === "GET" && pathname.startsWith("/api/findings/")) {
1269
- const project = decodeURIComponent(pathname.slice("/api/findings/".length));
1270
- if (!project || !isValidProjectName(project)) {
1271
- res.writeHead(400, { "content-type": "application/json" });
1272
- res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1273
- return;
1274
- }
1275
- const result = readFindings(phrenPath, project);
1276
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1277
- if (!result.ok) {
1278
- res.end(JSON.stringify({ ok: false, error: result.error }));
1279
- }
1280
- else {
1281
- res.end(JSON.stringify({ ok: true, data: { project, findings: result.data } }));
1282
- }
1283
- return;
1284
- }
1285
- // POST /api/findings/:project — add a finding
1286
- if (req.method === "POST" && pathname.startsWith("/api/findings/")) {
1287
- const project = decodeURIComponent(pathname.slice("/api/findings/".length));
1288
- if (!project || !isValidProjectName(project)) {
1289
- res.writeHead(400, { "content-type": "application/json" });
1290
- res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1291
- return;
1292
- }
1293
- void readFormBody(req, res).then((parsed) => {
1294
- if (!parsed)
1295
- return;
1296
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
1297
- return;
1298
- if (!requireCsrf(res, parsed, csrfTokens, true))
1299
- return;
1300
- const text = String(parsed.text || "");
1301
- if (!text) {
1302
- res.writeHead(200, { "content-type": "application/json" });
1303
- res.end(JSON.stringify({ ok: false, error: "text is required" }));
1304
- return;
1305
- }
1306
- const result = addFindingStore(phrenPath, project, text);
1307
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1308
- res.end(JSON.stringify({ ok: result.ok, message: result.ok ? result.data : undefined, error: result.ok ? undefined : result.error }));
1309
- });
1310
- return;
1311
- }
1312
- // PUT /api/findings/:project — edit a finding (old_text → new_text)
1313
- if (req.method === "PUT" && pathname.startsWith("/api/findings/")) {
1314
- const project = decodeURIComponent(pathname.slice("/api/findings/".length));
1315
- if (!project || !isValidProjectName(project)) {
1316
- res.writeHead(400, { "content-type": "application/json" });
1317
- res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1318
- return;
1319
- }
1320
- void readFormBody(req, res).then((parsed) => {
1321
- if (!parsed)
1322
- return;
1323
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
1324
- return;
1325
- if (!requireCsrf(res, parsed, csrfTokens, true))
1326
- return;
1327
- const oldText = String(parsed.old_text || "");
1328
- const newText = String(parsed.new_text || "");
1329
- if (!oldText || !newText) {
1330
- res.writeHead(200, { "content-type": "application/json" });
1331
- res.end(JSON.stringify({ ok: false, error: "old_text and new_text are required" }));
1332
- return;
1333
- }
1334
- const result = editFinding(phrenPath, project, oldText, newText);
1335
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1336
- res.end(JSON.stringify({ ok: result.ok, error: result.ok ? undefined : result.error }));
1337
- });
1338
- return;
1339
- }
1340
- // DELETE /api/findings/:project — remove a finding by text match
1341
- if (req.method === "DELETE" && pathname.startsWith("/api/findings/")) {
1342
- const project = decodeURIComponent(pathname.slice("/api/findings/".length));
1343
- if (!project || !isValidProjectName(project)) {
1344
- res.writeHead(400, { "content-type": "application/json" });
1345
- res.end(JSON.stringify({ ok: false, error: "Invalid project name" }));
1346
- return;
1347
- }
1348
- void readFormBody(req, res).then((parsed) => {
1349
- if (!parsed)
1350
- return;
1351
- if (!requirePostAuth(req, res, url, parsed, authToken, true))
1352
- return;
1353
- if (!requireCsrf(res, parsed, csrfTokens, true))
1354
- return;
1355
- const text = String(parsed.text || "");
1356
- if (!text) {
1357
- res.writeHead(200, { "content-type": "application/json" });
1358
- res.end(JSON.stringify({ ok: false, error: "text is required" }));
1359
- return;
1360
- }
1361
- const result = removeFinding(phrenPath, project, text);
1362
- res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1363
- res.end(JSON.stringify({ ok: result.ok, error: result.ok ? undefined : result.error }));
1364
- });
1365
- return;
1366
- }
1367
- res.writeHead(404, { "content-type": "text/plain" });
1368
- res.end("Not found");
1369
- });
1370
- }
1371
- export async function startWebUiServer(phrenPath, port, renderPage, profile, opts = {}) {
1372
- const authToken = crypto.randomUUID();
1373
- const csrfTokens = new Map();
1374
- const server = createWebUiHttpServer(phrenPath, renderPage, profile, { authToken, csrfTokens });
1375
- const boundPort = await bindWebUiPort(server, port, Boolean(opts.allowPortFallback && port !== 0));
1376
- const publicUrl = `http://127.0.0.1:${boundPort}`;
1377
- const reviewUrl = `${publicUrl}/?_auth=${encodeURIComponent(authToken)}`;
1378
- const ready = await waitForWebUiReady(reviewUrl);
1379
- process.stdout.write(`phren web-ui running at ${publicUrl}\n`);
1380
- process.stderr.write(`open: ${reviewUrl}\n`);
1381
- if (!ready) {
1382
- process.stderr.write("[phren] web-ui health check did not confirm readiness before launch\n");
1383
- }
1384
- const shouldAutoOpen = opts.autoOpen ?? Boolean(process.stdout.isTTY);
1385
- if (shouldAutoOpen && ready) {
1386
- try {
1387
- if (opts.browserLauncher)
1388
- await opts.browserLauncher(reviewUrl);
1389
- else
1390
- await launchWebUiBrowser(reviewUrl);
1391
- }
1392
- catch (err) {
1393
- process.stderr.write(`[phren] web-ui browser launch failed: ${errorMessage(err)}\n`);
1394
- process.stdout.write(`secure session URL: ${reviewUrl}\n`);
1395
- }
1396
- }
1397
- else if (shouldAutoOpen && !ready) {
1398
- process.stderr.write("[phren] skipped auto-open because readiness check failed; use the secure URL below\n");
1399
- process.stdout.write(`secure session URL: ${reviewUrl}\n`);
1400
- }
1401
- else {
1402
- process.stdout.write(`secure session URL: ${reviewUrl}\n`);
1403
- }
1404
- await new Promise((resolve) => {
1405
- const shutdown = () => {
1406
- server.close(() => resolve());
1407
- };
1408
- process.on("SIGTERM", shutdown);
1409
- process.on("SIGINT", shutdown);
1410
- });
1411
- }