@phren/cli 0.0.10 → 0.0.12

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 (100) hide show
  1. package/README.md +11 -17
  2. package/mcp/dist/capabilities/cli.js +1 -1
  3. package/mcp/dist/capabilities/mcp.js +1 -1
  4. package/mcp/dist/capabilities/vscode.js +1 -1
  5. package/mcp/dist/capabilities/web-ui.js +1 -1
  6. package/mcp/dist/cli-actions.js +58 -71
  7. package/mcp/dist/cli-config.js +337 -131
  8. package/mcp/dist/cli-extract.js +3 -2
  9. package/mcp/dist/cli-govern.js +35 -63
  10. package/mcp/dist/cli-graph.js +19 -4
  11. package/mcp/dist/cli-hooks-globs.js +2 -1
  12. package/mcp/dist/cli-hooks-output.js +4 -4
  13. package/mcp/dist/cli-hooks-session.js +1 -1
  14. package/mcp/dist/cli-hooks.js +44 -35
  15. package/mcp/dist/cli-namespaces.js +15 -5
  16. package/mcp/dist/cli-search.js +2 -2
  17. package/mcp/dist/cli.js +1 -1
  18. package/mcp/dist/content-archive.js +23 -14
  19. package/mcp/dist/content-citation.js +13 -2
  20. package/mcp/dist/content-dedup.js +9 -9
  21. package/mcp/dist/content-learning.js +6 -4
  22. package/mcp/dist/content-metadata.js +10 -0
  23. package/mcp/dist/core-finding.js +1 -1
  24. package/mcp/dist/data-access.js +10 -31
  25. package/mcp/dist/data-tasks.js +5 -26
  26. package/mcp/dist/embedding.js +7 -8
  27. package/mcp/dist/entrypoint.js +133 -102
  28. package/mcp/dist/finding-impact.js +1 -32
  29. package/mcp/dist/finding-journal.js +1 -1
  30. package/mcp/dist/finding-lifecycle.js +2 -7
  31. package/mcp/dist/governance-locks.js +12 -5
  32. package/mcp/dist/governance-policy.js +156 -9
  33. package/mcp/dist/governance-scores.js +4 -10
  34. package/mcp/dist/hooks.js +62 -18
  35. package/mcp/dist/index.js +4 -4
  36. package/mcp/dist/init-config.js +4 -25
  37. package/mcp/dist/init-preferences.js +1 -1
  38. package/mcp/dist/init-setup.js +6 -55
  39. package/mcp/dist/init-shared.js +53 -1
  40. package/mcp/dist/init.js +191 -29
  41. package/mcp/dist/link-checksums.js +3 -2
  42. package/mcp/dist/link-context.js +2 -2
  43. package/mcp/dist/link-doctor.js +14 -57
  44. package/mcp/dist/link-skills.js +98 -12
  45. package/mcp/dist/link.js +16 -75
  46. package/mcp/dist/machine-identity.js +1 -9
  47. package/mcp/dist/mcp-config.js +247 -42
  48. package/mcp/dist/mcp-data.js +9 -9
  49. package/mcp/dist/mcp-extract-facts.js +12 -7
  50. package/mcp/dist/mcp-extract.js +2 -2
  51. package/mcp/dist/mcp-finding.js +16 -20
  52. package/mcp/dist/mcp-graph.js +12 -12
  53. package/mcp/dist/mcp-hooks.js +1 -1
  54. package/mcp/dist/mcp-ops.js +18 -18
  55. package/mcp/dist/mcp-search.js +11 -16
  56. package/mcp/dist/mcp-session.js +12 -2
  57. package/mcp/dist/memory-ui-assets.js +1 -36
  58. package/mcp/dist/memory-ui-graph.js +152 -50
  59. package/mcp/dist/memory-ui-page.js +30 -5
  60. package/mcp/dist/memory-ui-scripts.js +252 -63
  61. package/mcp/dist/memory-ui-server.js +115 -3
  62. package/mcp/dist/phren-core.js +2 -0
  63. package/mcp/dist/phren-paths.js +8 -9
  64. package/mcp/dist/proactivity.js +5 -5
  65. package/mcp/dist/profile-store.js +2 -2
  66. package/mcp/dist/project-config.js +64 -17
  67. package/mcp/dist/provider-adapters.js +1 -1
  68. package/mcp/dist/query-correlation.js +22 -19
  69. package/mcp/dist/session-checkpoints.js +14 -14
  70. package/mcp/dist/session-utils.js +3 -2
  71. package/mcp/dist/shared-data-utils.js +28 -0
  72. package/mcp/dist/shared-fragment-graph.js +22 -21
  73. package/mcp/dist/shared-governance.js +1 -1
  74. package/mcp/dist/shared-index.js +144 -105
  75. package/mcp/dist/shared-retrieval.js +21 -23
  76. package/mcp/dist/shared-search-fallback.js +15 -25
  77. package/mcp/dist/shared-sqljs.js +3 -2
  78. package/mcp/dist/shared.js +5 -6
  79. package/mcp/dist/shell-entry.js +1 -1
  80. package/mcp/dist/shell-input.js +63 -53
  81. package/mcp/dist/shell-palette.js +6 -1
  82. package/mcp/dist/shell-render.js +9 -5
  83. package/mcp/dist/shell-state-store.js +2 -5
  84. package/mcp/dist/shell-view.js +7 -6
  85. package/mcp/dist/shell.js +5 -55
  86. package/mcp/dist/skill-files.js +4 -10
  87. package/mcp/dist/skill-registry.js +3 -0
  88. package/mcp/dist/status.js +43 -21
  89. package/mcp/dist/task-hygiene.js +1 -1
  90. package/mcp/dist/telemetry.js +5 -4
  91. package/mcp/dist/update.js +1 -1
  92. package/mcp/dist/utils.js +4 -4
  93. package/package.json +2 -3
  94. package/skills/docs.md +11 -11
  95. package/starter/README.md +1 -1
  96. package/starter/global/CLAUDE.md +2 -2
  97. package/starter/global/skills/audit.md +106 -0
  98. package/mcp/dist/cli-hooks-retrieval.js +0 -2
  99. package/mcp/dist/impact-scoring.js +0 -22
  100. package/mcp/dist/shared-paths.js +0 -1
@@ -9,6 +9,7 @@ import * as path from "path";
9
9
  import { debugLog } from "./shared.js";
10
10
  import { safeProjectPath, isFeatureEnabled, errorMessage } from "./utils.js";
11
11
  import { callLlm } from "./content-dedup.js";
12
+ import { withFileLock } from "./shared-governance.js";
12
13
  const FACT_EXTRACT_FLAG = "PHREN_FEATURE_FACT_EXTRACT";
13
14
  const MAX_FACTS = 50;
14
15
  function preferencesPath(phrenPath, project) {
@@ -24,7 +25,7 @@ export function readExtractedFacts(phrenPath, project) {
24
25
  return Array.isArray(data) ? data : [];
25
26
  }
26
27
  catch (err) {
27
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
28
+ if ((process.env.PHREN_DEBUG))
28
29
  process.stderr.write(`[phren] readExtractedFacts: ${errorMessage(err)}\n`);
29
30
  return [];
30
31
  }
@@ -64,13 +65,17 @@ export function extractFactFromFinding(phrenPath, project, finding) {
64
65
  const fact = raw.replace(/[\r\n]+/g, " ").trim().slice(0, 200);
65
66
  if (!fact)
66
67
  return;
67
- // Re-read inside the callback to minimize race window (best-effort; not locked)
68
- const existing = readExtractedFacts(phrenPath, project);
69
- const normalized = fact.toLowerCase();
70
- if (existing.some(f => f.fact.toLowerCase() === normalized))
68
+ const p = preferencesPath(phrenPath, project);
69
+ if (!p)
71
70
  return;
72
- existing.push({ fact, source: finding.slice(0, 120), at: new Date().toISOString() });
73
- writeExtractedFacts(phrenPath, project, existing);
71
+ withFileLock(p, () => {
72
+ const existing = readExtractedFacts(phrenPath, project);
73
+ const normalized = fact.toLowerCase();
74
+ if (existing.some(f => f.fact.toLowerCase() === normalized))
75
+ return;
76
+ existing.push({ fact, source: finding.slice(0, 120), at: new Date().toISOString() });
77
+ writeExtractedFacts(phrenPath, project, existing);
78
+ });
74
79
  })
75
80
  .catch((err) => {
76
81
  debugLog(`extractFactFromFinding: ${errorMessage(err)}`);
@@ -1,6 +1,6 @@
1
1
  import { mcpResponse } from "./mcp-types.js";
2
2
  import { z } from "zod";
3
- import { isValidProjectName, safeProjectPath } from "./utils.js";
3
+ import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
4
4
  import { addFindingsToFile } from "./shared-content.js";
5
5
  import { checkOllamaAvailable, checkModelAvailable, generateText, getOllamaUrl, getExtractModel } from "./shared-ollama.js";
6
6
  import { debugLog } from "./shared.js";
@@ -34,7 +34,7 @@ function parseFindings(raw) {
34
34
  }
35
35
  }
36
36
  catch (err) {
37
- debugLog(`auto_extract: failed to parse LLM output as JSON: ${cleaned.slice(0, 200)} (${err instanceof Error ? err.message : String(err)})`);
37
+ debugLog(`auto_extract: failed to parse LLM output as JSON: ${cleaned.slice(0, 200)} (${errorMessage(err)})`);
38
38
  }
39
39
  return [];
40
40
  }
@@ -4,7 +4,7 @@ import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
6
6
  import { removeFinding as removeFindingCore, removeFindings as removeFindingsCore, } from "./core-finding.js";
7
- import { debugLog, EXEC_TIMEOUT_MS, FINDING_TYPES, normalizeMemoryScope, } from "./shared.js";
7
+ import { debugLog, EXEC_TIMEOUT_MS, FINDING_TYPES, normalizeMemoryScope, RESERVED_PROJECT_DIR_NAMES, } from "./shared.js";
8
8
  import { addFindingToFile, addFindingsToFile, checkSemanticConflicts, autoMergeConflicts, } from "./shared-content.js";
9
9
  import { jaccardTokenize, jaccardSimilarity, stripMetadata } from "./content-dedup.js";
10
10
  import { runCustomHooks } from "./hooks.js";
@@ -17,7 +17,6 @@ import { FINDING_PROVENANCE_SOURCES } from "./content-citation.js";
17
17
  import { isInactiveFindingLine, supersedeFinding, retractFinding as retractFindingLifecycle, resolveFindingContradiction, } from "./finding-lifecycle.js";
18
18
  const JACCARD_MAYBE_LOW = 0.30;
19
19
  const JACCARD_MAYBE_HIGH = 0.55; // above this isDuplicateFinding already catches it
20
- const RESERVED_PROJECT_DIRS = new Set(["global", ".runtime", ".sessions", ".governance"]);
21
20
  function findJaccardCandidates(phrenPath, project, finding) {
22
21
  try {
23
22
  const findingsPath = path.join(phrenPath, project, "FINDINGS.md");
@@ -134,14 +133,11 @@ export function register(server, ctx) {
134
133
  if (!result.ok) {
135
134
  return mcpResponse({ ok: false, error: result.error });
136
135
  }
137
- // Determine status from the returned message string
138
- const isSkipped = result.data.startsWith("Skipped duplicate");
139
- const isAdded = !isSkipped;
140
- if (isSkipped) {
141
- return mcpResponse({ ok: true, message: result.data, data: { project, finding: taggedFinding, status: "skipped" } });
136
+ if (result.data.status === "skipped") {
137
+ return mcpResponse({ ok: true, message: result.data.message, data: { project, finding: taggedFinding, status: "skipped" } });
142
138
  }
143
139
  updateFileInIndex(path.join(phrenPath, project, "FINDINGS.md"));
144
- if (isAdded) {
140
+ if (result.data.status === "added" || result.data.status === "created") {
145
141
  runCustomHooks(phrenPath, "post-finding", { PHREN_PROJECT: project });
146
142
  incrementSessionFindings(phrenPath, 1, sessionId, project);
147
143
  extractFactFromFinding(phrenPath, project, taggedFinding);
@@ -178,7 +174,7 @@ export function register(server, ctx) {
178
174
  }
179
175
  const conflictsWithList = semanticConflicts.checked
180
176
  ? extractConflictsWith(semanticConflicts.annotations)
181
- : (result.data.match(/<!--\s*conflicts_with:\s*"([^"]+)"/)?.[1] ? [result.data.match(/<!--\s*conflicts_with:\s*"([^"]+)"/)[1]] : []);
177
+ : (result.data.message.match(/<!--\s*conflicts_with:\s*"([^"]+)"/)?.[1] ? [result.data.message.match(/<!--\s*conflicts_with:\s*"([^"]+)"/)[1]] : []);
182
178
  const conflictsWith = conflictsWithList[0];
183
179
  // Extract fragment hints synchronously from the finding text (regex only, no DB).
184
180
  // Full DB fragment linking happens on the next index rebuild via updateFileInIndex →
@@ -186,11 +182,11 @@ export function register(server, ctx) {
186
182
  const detectedFragments = extractFragmentNames(taggedFinding);
187
183
  return mcpResponse({
188
184
  ok: true,
189
- message: result.data,
185
+ message: result.data.message,
190
186
  data: {
191
187
  project,
192
188
  finding: taggedFinding,
193
- status: "added",
189
+ status: result.data.status,
194
190
  ...(conflictsWith ? { conflictsWith } : {}),
195
191
  ...(conflictsWithList.length > 0 ? { conflicts: conflictsWithList } : {}),
196
192
  ...(detectedFragments.length > 0 ? { detectedFragments } : {}),
@@ -236,7 +232,7 @@ export function register(server, ctx) {
236
232
  extraAnnotationsByFinding.push(conflicts.checked && conflicts.annotations.length > 0 ? conflicts.annotations : []);
237
233
  }
238
234
  catch (err) {
239
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
235
+ if ((process.env.PHREN_DEBUG))
240
236
  process.stderr.write(`[phren] add_findings semanticConflict: ${errorMessage(err)}\n`);
241
237
  extraAnnotationsByFinding.push([]);
242
238
  }
@@ -356,7 +352,7 @@ export function register(server, ctx) {
356
352
  const projects = project
357
353
  ? [project]
358
354
  : fs.readdirSync(phrenPath, { withFileTypes: true })
359
- .filter((entry) => entry.isDirectory() && !RESERVED_PROJECT_DIRS.has(entry.name) && isValidProjectName(entry.name))
355
+ .filter((entry) => entry.isDirectory() && !RESERVED_PROJECT_DIR_NAMES.has(entry.name) && isValidProjectName(entry.name))
360
356
  .map((entry) => entry.name);
361
357
  const contradictions = [];
362
358
  for (const p of projects) {
@@ -490,8 +486,8 @@ export function register(server, ctx) {
490
486
  .filter((name) => name && !name.startsWith(".") && name !== "profiles")));
491
487
  const commitMsg = message || `phren: save ${files.length} file(s) across ${projectNames.length} project(s)`;
492
488
  runCustomHooks(phrenPath, "pre-save");
493
- // Restrict to known phren file types to avoid staging .env or credential files
494
- runGit(["add", "--", "*.md", "*.json", "*.yaml", "*.yml", "*.jsonl", "*.txt"]);
489
+ // Stage all files including untracked (new project dirs, first FINDINGS.md, etc.)
490
+ runGit(["add", "-A"]);
495
491
  runGit(["commit", "-m", commitMsg]);
496
492
  let hasRemote = false;
497
493
  try {
@@ -499,7 +495,7 @@ export function register(server, ctx) {
499
495
  hasRemote = remotes.length > 0;
500
496
  }
501
497
  catch (err) {
502
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
498
+ if ((process.env.PHREN_DEBUG))
503
499
  process.stderr.write(`[phren] push_changes remoteCheck: ${errorMessage(err)}\n`);
504
500
  }
505
501
  if (!hasRemote) {
@@ -523,7 +519,7 @@ export function register(server, ctx) {
523
519
  runGit(["pull", "--rebase", "--quiet"], { timeout: 15000 });
524
520
  }
525
521
  catch (pullErr) {
526
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
522
+ if ((process.env.PHREN_DEBUG))
527
523
  process.stderr.write(`[phren] push_changes pullRebase: ${pullErr instanceof Error ? pullErr.message : String(pullErr)}\n`);
528
524
  const resolved = autoMergeConflicts(phrenPath);
529
525
  if (resolved) {
@@ -534,13 +530,13 @@ export function register(server, ctx) {
534
530
  });
535
531
  }
536
532
  catch (continueErr) {
537
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
533
+ if ((process.env.PHREN_DEBUG))
538
534
  process.stderr.write(`[phren] push_changes rebaseContinue: ${continueErr instanceof Error ? continueErr.message : String(continueErr)}\n`);
539
535
  try {
540
536
  runGit(["rebase", "--abort"]);
541
537
  }
542
538
  catch (abortErr) {
543
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
539
+ if ((process.env.PHREN_DEBUG))
544
540
  process.stderr.write(`[phren] push_changes rebaseAbort: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}\n`);
545
541
  }
546
542
  break;
@@ -551,7 +547,7 @@ export function register(server, ctx) {
551
547
  runGit(["rebase", "--abort"]);
552
548
  }
553
549
  catch (abortErr) {
554
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
550
+ if ((process.env.PHREN_DEBUG))
555
551
  process.stderr.write(`[phren] push_changes rebaseAbort2: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}\n`);
556
552
  }
557
553
  break;
@@ -2,14 +2,14 @@ import { mcpResponse } from "./mcp-types.js";
2
2
  import { z } from "zod";
3
3
  import * as fs from "fs";
4
4
  import * as crypto from "crypto";
5
- import { isValidProjectName } from "./utils.js";
5
+ import { isValidProjectName, errorMessage } from "./utils.js";
6
6
  import { queryDocBySourceKey, queryRows, queryFragmentLinks, queryCrossProjectFragments, ensureGlobalEntitiesTable, logFragmentMiss } from "./shared-index.js";
7
7
  import { runtimeFile } from "./shared.js";
8
8
  import { withFileLock } from "./shared-governance.js";
9
9
  export function register(server, ctx) {
10
10
  // ── search_fragments ──────────────────────────────────────────────────
11
11
  server.registerTool("search_fragments", {
12
- title: "phren : search fragments",
12
+ title: "phren · search fragments",
13
13
  description: "Search named fragments in the knowledge graph (libraries, tools, concepts mentioned in findings). " +
14
14
  "Returns matching fragment names and how many findings reference each.",
15
15
  inputSchema: z.object({
@@ -209,8 +209,8 @@ export function register(server, ctx) {
209
209
  db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [fragmentName, resolvedFragmentType, new Date().toISOString().slice(0, 10)]);
210
210
  }
211
211
  catch (err) {
212
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
213
- process.stderr.write(`[phren] link_findings fragmentInsert: ${err instanceof Error ? err.message : String(err)}\n`);
212
+ if (process.env.PHREN_DEBUG)
213
+ process.stderr.write(`[phren] link_findings fragmentInsert: ${errorMessage(err)}\n`);
214
214
  }
215
215
  const fragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [fragmentName, resolvedFragmentType]);
216
216
  if (!fragmentResult?.length || !fragmentResult[0]?.values?.length) {
@@ -232,8 +232,8 @@ export function register(server, ctx) {
232
232
  db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [sourceDoc, "document", new Date().toISOString().slice(0, 10)]);
233
233
  }
234
234
  catch (err) {
235
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
236
- process.stderr.write(`[phren] link_findings docFragmentInsert: ${err instanceof Error ? err.message : String(err)}\n`);
235
+ if (process.env.PHREN_DEBUG)
236
+ process.stderr.write(`[phren] link_findings docFragmentInsert: ${errorMessage(err)}\n`);
237
237
  }
238
238
  const docFragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [sourceDoc, "document"]);
239
239
  if (!docFragmentResult?.length || !docFragmentResult[0]?.values?.length) {
@@ -245,8 +245,8 @@ export function register(server, ctx) {
245
245
  db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [sourceId, targetId, relType, sourceDoc]);
246
246
  }
247
247
  catch (err) {
248
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
249
- process.stderr.write(`[phren] link_findings linkInsert: ${err instanceof Error ? err.message : String(err)}\n`);
248
+ if (process.env.PHREN_DEBUG)
249
+ process.stderr.write(`[phren] link_findings linkInsert: ${errorMessage(err)}\n`);
250
250
  return mcpResponse({ ok: false, error: "Failed to insert fragment link." });
251
251
  }
252
252
  // 4a. Also populate global_entities so manual links appear in cross_project_fragments
@@ -255,8 +255,8 @@ export function register(server, ctx) {
255
255
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [fragmentName, project, sourceDoc]);
256
256
  }
257
257
  catch (err) {
258
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
259
- process.stderr.write(`[phren] link_findings globalFragments: ${err instanceof Error ? err.message : String(err)}\n`);
258
+ if (process.env.PHREN_DEBUG)
259
+ process.stderr.write(`[phren] link_findings globalFragments: ${errorMessage(err)}\n`);
260
260
  }
261
261
  // 4b. Persist manual link so it survives index rebuilds (mandatory — failure aborts the operation)
262
262
  const manualLinksPath = runtimeFile(ctx.phrenPath, "manual-links.json");
@@ -268,8 +268,8 @@ export function register(server, ctx) {
268
268
  existing = JSON.parse(fs.readFileSync(manualLinksPath, "utf8"));
269
269
  }
270
270
  catch (err) {
271
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
272
- process.stderr.write(`[phren] link_findings manualLinksRead: ${err instanceof Error ? err.message : String(err)}\n`);
271
+ if (process.env.PHREN_DEBUG)
272
+ process.stderr.write(`[phren] link_findings manualLinksRead: ${errorMessage(err)}\n`);
273
273
  }
274
274
  }
275
275
  const newEntry = { entity: fragmentName, entityType: resolvedFragmentType, sourceDoc, relType };
@@ -23,7 +23,7 @@ function validateHookCommand(command) {
23
23
  return "Command too long (max 1000 characters).";
24
24
  // Reject shell metacharacters that allow injection or arbitrary execution
25
25
  // when the command is later run via `sh -c`.
26
- if (/[`$(){}&|;<>]/.test(trimmed)) {
26
+ if (/[`$(){}&|;<>\n\r#]/.test(trimmed)) {
27
27
  return "Command contains disallowed shell characters: ` $ ( ) { } & | ; < >";
28
28
  }
29
29
  // eval and source can execute arbitrary code
@@ -4,7 +4,7 @@ import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import { runtimeFile, getProjectDirs } from "./shared.js";
6
6
  import { findFtsCacheForPath } from "./shared-index.js";
7
- import { isValidProjectName } from "./utils.js";
7
+ import { isValidProjectName, errorMessage } from "./utils.js";
8
8
  import { readReviewQueue, readReviewQueueAcrossProjects } from "./data-access.js";
9
9
  import { addProjectFromPath } from "./core-project.js";
10
10
  import { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode } from "./project-config.js";
@@ -44,7 +44,7 @@ export function register(server, ctx) {
44
44
  catch (err) {
45
45
  return mcpResponse({
46
46
  ok: false,
47
- error: err instanceof Error ? err.message : String(err),
47
+ error: errorMessage(err),
48
48
  });
49
49
  }
50
50
  });
@@ -108,8 +108,8 @@ export function register(server, ctx) {
108
108
  version = pkg.version || "unknown";
109
109
  }
110
110
  catch (err) {
111
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
112
- process.stderr.write(`[phren] healthCheck version: ${err instanceof Error ? err.message : String(err)}\n`);
111
+ if ((process.env.PHREN_DEBUG))
112
+ process.stderr.write(`[phren] healthCheck version: ${errorMessage(err)}\n`);
113
113
  }
114
114
  // FTS index (lives in /tmpphren-fts-*/, not .runtime/)
115
115
  let indexStatus = { exists: false };
@@ -117,8 +117,8 @@ export function register(server, ctx) {
117
117
  indexStatus = findFtsCacheForPath(phrenPath, activeProfile);
118
118
  }
119
119
  catch (err) {
120
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
121
- process.stderr.write(`[phren] healthCheck ftsCacheCheck: ${err instanceof Error ? err.message : String(err)}\n`);
120
+ if ((process.env.PHREN_DEBUG))
121
+ process.stderr.write(`[phren] healthCheck ftsCacheCheck: ${errorMessage(err)}\n`);
122
122
  }
123
123
  // Hook registration
124
124
  let hooksEnabled = false;
@@ -127,8 +127,8 @@ export function register(server, ctx) {
127
127
  hooksEnabled = getHooksEnabledPreference(phrenPath);
128
128
  }
129
129
  catch (err) {
130
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
131
- process.stderr.write(`[phren] healthCheck hooksEnabled: ${err instanceof Error ? err.message : String(err)}\n`);
130
+ if ((process.env.PHREN_DEBUG))
131
+ process.stderr.write(`[phren] healthCheck hooksEnabled: ${errorMessage(err)}\n`);
132
132
  }
133
133
  let mcpEnabled = false;
134
134
  try {
@@ -136,8 +136,8 @@ export function register(server, ctx) {
136
136
  mcpEnabled = getMcpEnabledPreference(phrenPath);
137
137
  }
138
138
  catch (err) {
139
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
140
- process.stderr.write(`[phren] healthCheck mcpEnabled: ${err instanceof Error ? err.message : String(err)}\n`);
139
+ if ((process.env.PHREN_DEBUG))
140
+ process.stderr.write(`[phren] healthCheck mcpEnabled: ${errorMessage(err)}\n`);
141
141
  }
142
142
  // Profile/machine info
143
143
  const machineName = (() => {
@@ -145,8 +145,8 @@ export function register(server, ctx) {
145
145
  return getMachineName();
146
146
  }
147
147
  catch (err) {
148
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
149
- process.stderr.write(`[phren] healthCheck machineName: ${err instanceof Error ? err.message : String(err)}\n`);
148
+ if ((process.env.PHREN_DEBUG))
149
+ process.stderr.write(`[phren] healthCheck machineName: ${errorMessage(err)}\n`);
150
150
  }
151
151
  return undefined;
152
152
  })();
@@ -160,8 +160,8 @@ export function register(server, ctx) {
160
160
  taskMode = workflowPolicy.taskMode;
161
161
  }
162
162
  catch (err) {
163
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
164
- process.stderr.write(`[phren] healthCheck taskMode: ${err instanceof Error ? err.message : String(err)}\n`);
163
+ if ((process.env.PHREN_DEBUG))
164
+ process.stderr.write(`[phren] healthCheck taskMode: ${errorMessage(err)}\n`);
165
165
  }
166
166
  try {
167
167
  const { readInstallPreferences } = await import("./init-preferences.js");
@@ -169,8 +169,8 @@ export function register(server, ctx) {
169
169
  proactivity = prefs.proactivity || "high";
170
170
  }
171
171
  catch (err) {
172
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
173
- process.stderr.write(`[phren] healthCheck proactivity: ${err instanceof Error ? err.message : String(err)}\n`);
172
+ if ((process.env.PHREN_DEBUG))
173
+ process.stderr.write(`[phren] healthCheck proactivity: ${errorMessage(err)}\n`);
174
174
  }
175
175
  const lines = [
176
176
  `Phren v${version}`,
@@ -262,8 +262,8 @@ export function register(server, ctx) {
262
262
  return lines.filter(line => ERROR_PATTERNS.some(p => p.test(line)));
263
263
  }
264
264
  catch (err) {
265
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
266
- process.stderr.write(`[phren] readErrorLines: ${err instanceof Error ? err.message : String(err)}\n`);
265
+ if ((process.env.PHREN_DEBUG))
266
+ process.stderr.write(`[phren] readErrorLines: ${errorMessage(err)}\n`);
267
267
  return [];
268
268
  }
269
269
  }
@@ -35,8 +35,8 @@ export function logSearchMiss(phrenPath, query, project) {
35
35
  fs.appendFileSync(missFile, entry + "\n");
36
36
  }
37
37
  catch (err) {
38
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
39
- process.stderr.write(`[phren] logSearchMiss: ${err instanceof Error ? err.message : String(err)}\n`);
38
+ if ((process.env.PHREN_DEBUG))
39
+ process.stderr.write(`[phren] logSearchMiss: ${errorMessage(err)}\n`);
40
40
  }
41
41
  }
42
42
  const HISTORY_FINDING_STATUSES = new Set(["superseded", "retracted"]);
@@ -160,7 +160,7 @@ export function register(server, ctx) {
160
160
  createdAt = stat.birthtime.toISOString();
161
161
  }
162
162
  catch (err) {
163
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
163
+ if ((process.env.PHREN_DEBUG))
164
164
  process.stderr.write(`[phren] search_knowledge statFile: ${errorMessage(err)}\n`);
165
165
  }
166
166
  // Extract tags from content (e.g. [decision], [pitfall], [pattern])
@@ -393,8 +393,8 @@ export function register(server, ctx) {
393
393
  relatedFragments = [...new Set(relatedFragments)].slice(0, 10);
394
394
  }
395
395
  catch (err) {
396
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
397
- process.stderr.write(`[phren] fragment query: ${err instanceof Error ? err.message : String(err)}\n`);
396
+ if ((process.env.PHREN_DEBUG))
397
+ process.stderr.write(`[phren] fragment query: ${errorMessage(err)}\n`);
398
398
  }
399
399
  const formatted = results.map((r) => `### ${r.project}/${r.filename} (${r.type})\n${r.snippet}\n\n\`${r.path}\``);
400
400
  // Memory synthesis: generate a concise paragraph from top results when requested
@@ -408,7 +408,7 @@ export function register(server, ctx) {
408
408
  synthCache = JSON.parse(fs.readFileSync(synthCachePath, "utf8"));
409
409
  }
410
410
  catch (err) {
411
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
411
+ if ((process.env.PHREN_DEBUG))
412
412
  process.stderr.write(`[phren] search_knowledge synthCacheRead: ${errorMessage(err)}\n`);
413
413
  }
414
414
  const cached = synthCache[synthKey];
@@ -433,8 +433,8 @@ export function register(server, ctx) {
433
433
  fs.writeFileSync(synthCachePath, JSON.stringify(synthCache));
434
434
  }
435
435
  catch (err) {
436
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
437
- process.stderr.write(`[phren] synthCache write: ${err instanceof Error ? err.message : String(err)}\n`);
436
+ if ((process.env.PHREN_DEBUG))
437
+ process.stderr.write(`[phren] synthCache write: ${errorMessage(err)}\n`);
438
438
  }
439
439
  }
440
440
  }
@@ -567,17 +567,12 @@ export function register(server, ctx) {
567
567
  if (!isValidProjectName(project))
568
568
  return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
569
569
  const includeHistory = include_history ?? include_superseded ?? false;
570
- const result = readFindings(phrenPath, project, { includeArchived: includeHistory });
570
+ // Always read with archive so we can compute historyCount without a second read
571
+ const result = readFindings(phrenPath, project, { includeArchived: true });
571
572
  if (!result.ok)
572
573
  return mcpResponse({ ok: false, error: result.error });
573
574
  const allItems = result.data;
574
- let historyCount = allItems.filter(f => f.tier === "archived" || HISTORY_FINDING_STATUSES.has(f.status)).length;
575
- if (!includeHistory) {
576
- const withArchive = readFindings(phrenPath, project, { includeArchived: true });
577
- if (withArchive.ok) {
578
- historyCount = withArchive.data.filter(f => f.tier === "archived" || HISTORY_FINDING_STATUSES.has(f.status)).length;
579
- }
580
- }
575
+ const historyCount = allItems.filter(f => f.tier === "archived" || HISTORY_FINDING_STATUSES.has(f.status)).length;
581
576
  const visibleItems = includeHistory
582
577
  ? allItems
583
578
  : allItems.filter(f => f.tier !== "archived" && !HISTORY_FINDING_STATUSES.has(f.status));
@@ -90,6 +90,7 @@ function extractResumptionHint(summary, fallbackNextStep, fallbackLastAttempt) {
90
90
  };
91
91
  }
92
92
  /** Per-connection session map keyed by arbitrary connection ID (if provided). */
93
+ const MAX_SESSION_MAP_ENTRIES = 200;
93
94
  const _sessionMap = new Map();
94
95
  function sessionsDir(phrenPath) {
95
96
  const dir = path.join(phrenPath, ".runtime", "sessions");
@@ -151,7 +152,10 @@ function lastSummaryPath(phrenPath) {
151
152
  function writeLastSummary(phrenPath, summary, sessionId, project) {
152
153
  try {
153
154
  const data = { summary, sessionId, project, endedAt: new Date().toISOString() };
154
- fs.writeFileSync(lastSummaryPath(phrenPath), JSON.stringify(data, null, 2));
155
+ const summaryFile = lastSummaryPath(phrenPath);
156
+ const tmpPath = `${summaryFile}.tmp-${crypto.randomUUID()}`;
157
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
158
+ fs.renameSync(tmpPath, summaryFile);
155
159
  }
156
160
  catch (err) {
157
161
  debugError("writeLastSummary", err);
@@ -417,8 +421,14 @@ export function register(server, ctx) {
417
421
  };
418
422
  const newFile = sessionFileForId(phrenPath, sessionId);
419
423
  writeSessionStateFile(newFile, next);
420
- if (connectionId)
424
+ if (connectionId) {
421
425
  _sessionMap.set(connectionId, sessionId);
426
+ if (_sessionMap.size > MAX_SESSION_MAP_ENTRIES) {
427
+ const oldest = _sessionMap.keys().next().value;
428
+ if (oldest !== undefined)
429
+ _sessionMap.delete(oldest);
430
+ }
431
+ }
422
432
  const parts = [];
423
433
  if (priorSummary) {
424
434
  parts.push(`## Last session\n${priorSummary}`);