@phren/cli 0.0.27 → 0.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/mcp/dist/capabilities/cli.js +2 -5
  2. package/mcp/dist/capabilities/mcp.js +5 -8
  3. package/mcp/dist/capabilities/types.js +2 -5
  4. package/mcp/dist/capabilities/vscode.js +2 -5
  5. package/mcp/dist/capabilities/web-ui.js +2 -5
  6. package/mcp/dist/{cli-actions.js → cli/actions.js} +22 -21
  7. package/mcp/dist/{cli.js → cli/cli.js} +13 -13
  8. package/mcp/dist/{cli-config.js → cli/config.js} +9 -9
  9. package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
  10. package/mcp/dist/{cli-govern.js → cli/govern.js} +10 -9
  11. package/mcp/dist/{cli-graph.js → cli/graph.js} +10 -9
  12. package/mcp/dist/{cli-hooks-citations.js → cli/hooks-citations.js} +2 -2
  13. package/mcp/dist/{cli-hooks-context.js → cli/hooks-context.js} +23 -23
  14. package/mcp/dist/{cli-hooks-globs.js → cli/hooks-globs.js} +4 -4
  15. package/mcp/dist/{cli-hooks-output.js → cli/hooks-output.js} +9 -10
  16. package/mcp/dist/{cli-hooks-session.js → cli/hooks-session.js} +42 -57
  17. package/mcp/dist/{cli-hooks.js → cli/hooks.js} +27 -26
  18. package/mcp/dist/{cli-namespaces.js → cli/namespaces.js} +25 -24
  19. package/mcp/dist/{cli-ops.js → cli/ops.js} +9 -9
  20. package/mcp/dist/{cli-search.js → cli/search.js} +8 -7
  21. package/mcp/dist/cli-hooks-git.js +243 -0
  22. package/mcp/dist/cli-hooks-prompt.js +319 -0
  23. package/mcp/dist/cli-hooks-session-handlers.js +349 -0
  24. package/mcp/dist/cli-hooks-stop.js +557 -0
  25. package/mcp/dist/{content-archive.js → content/archive.js} +8 -9
  26. package/mcp/dist/{content-citation.js → content/citation.js} +5 -5
  27. package/mcp/dist/{content-dedup.js → content/dedup.js} +9 -12
  28. package/mcp/dist/{content-learning.js → content/learning.js} +12 -12
  29. package/mcp/dist/{content-validate.js → content/validate.js} +5 -5
  30. package/mcp/dist/{core-finding.js → core/finding.js} +4 -4
  31. package/mcp/dist/{core-project.js → core/project.js} +4 -4
  32. package/mcp/dist/{core-search.js → core/search.js} +2 -2
  33. package/mcp/dist/{data-access.js → data/access.js} +131 -13
  34. package/mcp/dist/{data-tasks.js → data/tasks.js} +7 -5
  35. package/mcp/dist/embedding.js +9 -14
  36. package/mcp/dist/entrypoint.js +13 -11
  37. package/mcp/dist/{finding-context.js → finding/context.js} +2 -2
  38. package/mcp/dist/{finding-impact.js → finding/impact.js} +3 -3
  39. package/mcp/dist/{finding-journal.js → finding/journal.js} +4 -4
  40. package/mcp/dist/{finding-lifecycle.js → finding/lifecycle.js} +4 -4
  41. package/mcp/dist/{governance-audit.js → governance/audit.js} +2 -2
  42. package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
  43. package/mcp/dist/{governance-policy.js → governance/policy.js} +10 -12
  44. package/mcp/dist/{governance-rbac.js → governance/rbac.js} +3 -3
  45. package/mcp/dist/{governance-scores.js → governance/scores.js} +8 -10
  46. package/mcp/dist/hooks.js +39 -31
  47. package/mcp/dist/index-query.js +4 -1
  48. package/mcp/dist/index.js +53 -29
  49. package/mcp/dist/{init-config.js → init/config.js} +6 -6
  50. package/mcp/dist/{init.js → init/init.js} +28 -29
  51. package/mcp/dist/{init-preferences.js → init/preferences.js} +3 -3
  52. package/mcp/dist/{init-setup.js → init/setup.js} +17 -19
  53. package/mcp/dist/{init-shared.js → init/shared.js} +3 -3
  54. package/mcp/dist/init-bootstrap.js +68 -0
  55. package/mcp/dist/init-detect.js +38 -0
  56. package/mcp/dist/init-dryrun.js +55 -0
  57. package/mcp/dist/init-env.js +114 -0
  58. package/mcp/dist/init-fresh.js +239 -0
  59. package/mcp/dist/init-hooks.js +26 -0
  60. package/mcp/dist/init-mcp.js +65 -0
  61. package/mcp/dist/init-migrate.js +51 -0
  62. package/mcp/dist/init-modes.js +135 -0
  63. package/mcp/dist/init-npm.js +37 -0
  64. package/mcp/dist/init-project-local.js +99 -0
  65. package/mcp/dist/init-semantic.js +48 -0
  66. package/mcp/dist/init-types.js +1 -0
  67. package/mcp/dist/init-uninstall.js +482 -0
  68. package/mcp/dist/init-update.js +96 -0
  69. package/mcp/dist/init-walkthrough-merge.js +90 -0
  70. package/mcp/dist/init-walkthrough.js +529 -0
  71. package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
  72. package/mcp/dist/{link-context.js → link/context.js} +4 -4
  73. package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
  74. package/mcp/dist/{link.js → link/link.js} +26 -31
  75. package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
  76. package/mcp/dist/logger.js +11 -3
  77. package/mcp/dist/phren-art.js +0 -6
  78. package/mcp/dist/phren-paths.js +30 -12
  79. package/mcp/dist/proactivity.js +2 -2
  80. package/mcp/dist/profile-store.js +5 -6
  81. package/mcp/dist/project-config.js +2 -2
  82. package/mcp/dist/project-topics.js +1 -1
  83. package/mcp/dist/query-correlation.js +1 -1
  84. package/mcp/dist/{session-checkpoints.js → session/checkpoints.js} +3 -3
  85. package/mcp/dist/{session-utils.js → session/utils.js} +1 -1
  86. package/mcp/dist/{shared-content.js → shared/content.js} +7 -7
  87. package/mcp/dist/{shared-data-utils.js → shared/data-utils.js} +3 -3
  88. package/mcp/dist/{shared-embedding-cache.js → shared/embedding-cache.js} +3 -3
  89. package/mcp/dist/{shared-fragment-graph.js → shared/fragment-graph.js} +15 -24
  90. package/mcp/dist/shared/governance.js +4 -0
  91. package/mcp/dist/{shared-index.js → shared/index.js} +92 -123
  92. package/mcp/dist/{shared-ollama.js → shared/ollama.js} +2 -2
  93. package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +16 -21
  94. package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +17 -20
  95. package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
  96. package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
  97. package/mcp/dist/shared.js +4 -59
  98. package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
  99. package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
  100. package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
  101. package/mcp/dist/{shell-render.js → shell/render.js} +1 -1
  102. package/mcp/dist/{shell.js → shell/shell.js} +11 -11
  103. package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
  104. package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
  105. package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
  106. package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
  107. package/mcp/dist/{skill-registry.js → skill/registry.js} +4 -4
  108. package/mcp/dist/{skill-state.js → skill/state.js} +1 -1
  109. package/mcp/dist/startup-embedding.js +2 -2
  110. package/mcp/dist/status.js +15 -14
  111. package/mcp/dist/{tasks-github.js → task/github.js} +2 -2
  112. package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
  113. package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +7 -7
  114. package/mcp/dist/telemetry.js +3 -4
  115. package/mcp/dist/tool-registry.js +29 -17
  116. package/mcp/dist/tools/config.js +515 -0
  117. package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
  118. package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
  119. package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
  120. package/mcp/dist/{mcp-finding.js → tools/finding.js} +97 -124
  121. package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
  122. package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
  123. package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
  124. package/mcp/dist/{mcp-ops.js → tools/ops.js} +169 -71
  125. package/mcp/dist/{mcp-search.js → tools/search.js} +19 -23
  126. package/mcp/dist/{mcp-session.js → tools/session.js} +48 -23
  127. package/mcp/dist/{mcp-skills.js → tools/skills.js} +33 -35
  128. package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
  129. package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
  130. package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
  131. package/mcp/dist/{memory-ui-page.js → ui/page.js} +4 -6
  132. package/mcp/dist/{memory-ui-server.js → ui/server.js} +30 -22
  133. package/mcp/dist/update.js +2 -2
  134. package/mcp/dist/utils.js +51 -11
  135. package/package.json +17 -11
  136. package/scripts/preuninstall.mjs +139 -0
  137. package/starter/global/CLAUDE.md +3 -2
  138. package/mcp/dist/mcp-config.js +0 -551
  139. package/mcp/dist/shared-governance.js +0 -4
  140. package/starter/global/skills/pipeline.md +0 -35
  141. package/starter/global/skills/release.md +0 -35
  142. /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
  143. /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
  144. /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
  145. /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
  146. /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
  147. /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
  148. /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
  149. /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
@@ -1,8 +1,7 @@
1
- import { WEB_UI_STYLES, renderWebUiScript } from "./memory-ui-assets.js";
2
- import { renderGraphScript } from "./memory-ui-graph.js";
3
- import { readSyncSnapshot } from "./memory-ui-data.js";
4
- import { PROJECT_REFERENCE_UI_STYLES, REVIEW_UI_STYLES, SETTINGS_TAB_UI_STYLES, TASK_UI_STYLES } from "./memory-ui-styles.js";
5
- import { renderSharedWebUiHelpers, renderSkillUiEnhancementScript, renderProjectReferenceEnhancementScript, renderTasksAndSettingsScript, renderSearchScript, renderEventWiringScript, renderGraphHostScript, } from "./memory-ui-scripts.js";
1
+ import { WEB_UI_STYLES, renderWebUiScript } from "./assets.js";
2
+ import { renderGraphScript } from "./graph.js";
3
+ import { PROJECT_REFERENCE_UI_STYLES, REVIEW_UI_STYLES, SETTINGS_TAB_UI_STYLES, TASK_UI_STYLES } from "./styles.js";
4
+ import { renderSharedWebUiHelpers, renderSkillUiEnhancementScript, renderProjectReferenceEnhancementScript, renderTasksAndSettingsScript, renderSearchScript, renderEventWiringScript, renderGraphHostScript, } from "./scripts.js";
6
5
  function h(s) {
7
6
  return s
8
7
  .replace(/&/g, "&")
@@ -11,7 +10,6 @@ function h(s) {
11
10
  .replace(/"/g, """);
12
11
  }
13
12
  export function renderWebUiPage(phrenPath, authToken, nonce) {
14
- const sync = readSyncSnapshot(phrenPath);
15
13
  const nonceAttr = nonce ? ` nonce="${h(nonce)}"` : "";
16
14
  return `<!doctype html>
17
15
  <html lang="en">
@@ -5,18 +5,19 @@ import * as fs from "fs";
5
5
  import * as path from "path";
6
6
  import * as querystring from "querystring";
7
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";
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, isAllowedSkillPath, readSyncSnapshot, recentAccepted, recentUsage, } from "./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
+ import { logger } from "../logger.js";
20
21
  const CSRF_TOKEN_TTL_MS = 15 * 60 * 1000;
21
22
  const MAX_FORM_BODY_BYTES = 1_048_576;
22
23
  const WEB_UI_READY_ATTEMPTS = 12;
@@ -102,7 +103,7 @@ async function bindWebUiPort(server, requestedPort, allowPortFallback) {
102
103
  const candidate = candidates[i];
103
104
  try {
104
105
  if (candidate !== requestedPort) {
105
- process.stderr.write(`[phren] web-ui port ${candidate - 1} is busy, retrying on ${candidate}\n`);
106
+ logger.info("web-ui", `port ${candidate - 1} is busy, retrying on ${candidate}`);
106
107
  }
107
108
  return await listenOnLoopback(server, candidate);
108
109
  }
@@ -292,8 +293,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
292
293
  repairPreexistingInstall(phrenPath);
293
294
  }
294
295
  catch (err) {
295
- if ((process.env.PHREN_DEBUG))
296
- process.stderr.write(`[phren] web-ui repair: ${errorMessage(err)}\n`);
296
+ logger.debug("web-ui", `web-ui repair: ${errorMessage(err)}`);
297
297
  }
298
298
  const authToken = opts?.authToken;
299
299
  const csrfTokens = opts?.csrfTokens;
@@ -351,7 +351,15 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
351
351
  return;
352
352
  }
353
353
  runGit(["add", "--", "*.md", "*.json", "*.yaml", "*.yml", "*.jsonl", "*.txt"]);
354
- runGit(["commit", "-m", message]);
354
+ // Use --only to commit exactly the files we just staged,
355
+ // avoiding committing unrelated previously-staged changes.
356
+ const stagedFiles = runGit(["diff", "--cached", "--name-only"]);
357
+ if (!stagedFiles) {
358
+ res.writeHead(200, { "content-type": "application/json" });
359
+ res.end(JSON.stringify({ ok: true, message: "Nothing to sync — no matching files to commit." }));
360
+ return;
361
+ }
362
+ runGit(["commit", "-m", message, "--only", "--", ...stagedFiles.split("\n").filter(Boolean)]);
355
363
  let pushed = false;
356
364
  try {
357
365
  const remotes = runGit(["remote"]);
@@ -565,7 +573,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
565
573
  if (req.method === "GET" && pathname.startsWith("/api/skill-content")) {
566
574
  const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
567
575
  const filePath = String(qs.path || "");
568
- if (!filePath || !isAllowedFilePath(filePath, phrenPath)) {
576
+ if (!filePath || !isAllowedSkillPath(filePath, phrenPath)) {
569
577
  res.writeHead(400, { "content-type": "application/json" });
570
578
  res.end(JSON.stringify({ ok: false, error: "Invalid path" }));
571
579
  return;
@@ -594,7 +602,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
594
602
  return;
595
603
  const filePath = String(parsed.path || "");
596
604
  const content = String(parsed.content || "");
597
- if (!filePath || !isAllowedFilePath(filePath, phrenPath)) {
605
+ if (!filePath || !isAllowedSkillPath(filePath, phrenPath)) {
598
606
  res.writeHead(200, { "content-type": "application/json" });
599
607
  res.end(JSON.stringify({ ok: false, error: "Invalid path" }));
600
608
  return;
@@ -800,7 +808,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
800
808
  return;
801
809
  }
802
810
  try {
803
- const { runSearch } = await import("./cli-search.js");
811
+ const { runSearch } = await import("../cli/search.js");
804
812
  const result = await runSearch({ query, limit: Math.min(searchLimit, 50), project: searchProject, type: searchType }, phrenPath, profile || "");
805
813
  // Build file date map from source headers like [project/filename]
806
814
  const fileDates = {};
@@ -1379,7 +1387,7 @@ export async function startWebUiServer(phrenPath, port, renderPage, profile, opt
1379
1387
  process.stdout.write(`phren web-ui running at ${publicUrl}\n`);
1380
1388
  process.stderr.write(`open: ${reviewUrl}\n`);
1381
1389
  if (!ready) {
1382
- process.stderr.write("[phren] web-ui health check did not confirm readiness before launch\n");
1390
+ logger.warn("web-ui", "health check did not confirm readiness before launch");
1383
1391
  }
1384
1392
  const shouldAutoOpen = opts.autoOpen ?? Boolean(process.stdout.isTTY);
1385
1393
  if (shouldAutoOpen && ready) {
@@ -1390,12 +1398,12 @@ export async function startWebUiServer(phrenPath, port, renderPage, profile, opt
1390
1398
  await launchWebUiBrowser(reviewUrl);
1391
1399
  }
1392
1400
  catch (err) {
1393
- process.stderr.write(`[phren] web-ui browser launch failed: ${errorMessage(err)}\n`);
1401
+ logger.warn("web-ui", `browser launch failed: ${errorMessage(err)}`);
1394
1402
  process.stdout.write(`secure session URL: ${reviewUrl}\n`);
1395
1403
  }
1396
1404
  }
1397
1405
  else if (shouldAutoOpen && !ready) {
1398
- process.stderr.write("[phren] skipped auto-open because readiness check failed; use the secure URL below\n");
1406
+ logger.warn("web-ui", "skipped auto-open because readiness check failed; use the secure URL below");
1399
1407
  process.stdout.write(`secure session URL: ${reviewUrl}\n`);
1400
1408
  }
1401
1409
  else {
@@ -4,6 +4,7 @@ import { execFileSync } from "child_process";
4
4
  import { fileURLToPath } from "url";
5
5
  import { errorMessage } from "./utils.js";
6
6
  import { PACKAGE_NAME, PACKAGE_SPEC } from "./package-metadata.js";
7
+ import { logger } from "./logger.js";
7
8
  function shellCommand(bin) {
8
9
  return process.platform === "win32" ? `${bin}.cmd` : bin;
9
10
  }
@@ -63,8 +64,7 @@ export async function runPhrenUpdate(opts = {}) {
63
64
  }
64
65
  }
65
66
  catch (err) {
66
- if ((process.env.PHREN_DEBUG))
67
- process.stderr.write(`[phren] runPhrenUpdate gitStatus: ${errorMessage(err)}\n`);
67
+ logger.debug("runPhrenUpdate gitStatus", errorMessage(err));
68
68
  }
69
69
  const pull = run("git", ["pull", "--rebase", "--autostash"], root);
70
70
  run(shellCommand("npm"), ["install"], root);
package/mcp/dist/utils.js CHANGED
@@ -5,6 +5,28 @@ import * as yaml from "js-yaml";
5
5
  import { fileURLToPath } from "url";
6
6
  import { findPhrenPath } from "./phren-paths.js";
7
7
  import { bootstrapPhrenDotEnv } from "./phren-dotenv.js";
8
+ // Lazy import of logDebug to break circular dependency:
9
+ // utils.ts -> phren-paths.ts -> logger.ts -> phren-paths.ts -> utils.ts
10
+ let _logDebug;
11
+ async function ensureLogDebug() {
12
+ if (!_logDebug) {
13
+ try {
14
+ const mod = await import("./logger.js");
15
+ _logDebug = mod.logger.debug;
16
+ }
17
+ catch {
18
+ _logDebug = () => { };
19
+ }
20
+ }
21
+ }
22
+ function getLogDebug() {
23
+ if (!_logDebug) {
24
+ // Kick off the async import for future calls; fall back to no-op for this call
25
+ void ensureLogDebug();
26
+ return () => { };
27
+ }
28
+ return _logDebug;
29
+ }
8
30
  const _moduleDir = path.dirname(fileURLToPath(import.meta.url));
9
31
  function loadSynonymsJson(fileName) {
10
32
  const filePath = path.join(_moduleDir, fileName);
@@ -12,8 +34,7 @@ function loadSynonymsJson(fileName) {
12
34
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
13
35
  }
14
36
  catch (err) {
15
- if ((process.env.PHREN_DEBUG))
16
- process.stderr.write(`[phren] ${fileName} load failed: ${err instanceof Error ? err.message : String(err)}\n`);
37
+ getLogDebug()("loadSynonymsJson", `${fileName} load failed: ${err instanceof Error ? err.message : String(err)}`);
17
38
  return {};
18
39
  }
19
40
  }
@@ -244,10 +265,7 @@ export function sanitizeFts5Query(raw) {
244
265
  let q = raw.replace(/[^a-zA-Z0-9 \-"*]/g, " ");
245
266
  q = q.replace(/\s+/g, " ");
246
267
  q = q.trim();
247
- // Q83: FTS5 only accepts * as a prefix operator directly attached to a token
248
- // (e.g. "foo*"). A bare trailing asterisk (or lone "*") produces invalid
249
- // FTS5 syntax. Strip any asterisk that is not immediately preceded by a
250
- // word character so the query remains valid.
268
+ // Q83: see docs/decisions/Q83-fts5-asterisk-validation.md
251
269
  q = q.replace(/(?<!\w)\*/g, "");
252
270
  // Also strip a trailing asterisk that is preceded only by whitespace at word
253
271
  // end of the whole query (handles "foo *" → "foo").
@@ -276,8 +294,7 @@ function parseSynonymsYaml(filePath) {
276
294
  return loaded;
277
295
  }
278
296
  catch (err) {
279
- if ((process.env.PHREN_DEBUG))
280
- process.stderr.write(`[phren] synonyms.yaml parse failed (${filePath}): ${err instanceof Error ? err.message : String(err)}\n`);
297
+ getLogDebug()("parseSynonymsYaml", `synonyms.yaml parse failed (${filePath}): ${err instanceof Error ? err.message : String(err)}`);
281
298
  return {};
282
299
  }
283
300
  }
@@ -315,8 +332,7 @@ function parseLearnedSynonymsJson(filePath) {
315
332
  return loaded;
316
333
  }
317
334
  catch (err) {
318
- if ((process.env.PHREN_DEBUG))
319
- process.stderr.write(`[phren] learned-synonyms parse failed (${filePath}): ${err instanceof Error ? err.message : String(err)}\n`);
335
+ getLogDebug()("parseLearnedSynonymsJson", `learned-synonyms parse failed (${filePath}): ${err instanceof Error ? err.message : String(err)}`);
320
336
  return {};
321
337
  }
322
338
  }
@@ -512,8 +528,21 @@ export function buildRobustFtsQuery(raw, project, phrenPath) {
512
528
  // nothing; it trades precision for recall while staying in the FTS index.
513
529
  export function buildRelaxedFtsQuery(raw, project, phrenPath) {
514
530
  const clauses = buildFtsClauses(raw, project, phrenPath);
515
- if (clauses.length < 3)
531
+ if (clauses.length === 0)
516
532
  return "";
533
+ // Short queries (1-2 terms): OR the clauses together with prefix expansion
534
+ if (clauses.length === 1) {
535
+ const term = clauses[0];
536
+ // Add prefix wildcard for unquoted-style terms to broaden recall
537
+ const inner = term.replace(/^"(.*)"$/, "$1");
538
+ if (inner.length >= 3) {
539
+ return `(${term} OR "${inner}"*)`;
540
+ }
541
+ return term;
542
+ }
543
+ if (clauses.length === 2) {
544
+ return `(${clauses[0]} OR ${clauses[1]})`;
545
+ }
517
546
  const salientClauses = clauses
518
547
  .map((clause, index) => ({ clause, index, score: clauseSignalScore(clause) }))
519
548
  .sort((a, b) => {
@@ -539,5 +568,16 @@ export function buildFtsQueryVariants(raw, project, phrenPath) {
539
568
  buildRobustFtsQuery(raw, project, phrenPath),
540
569
  buildRelaxedFtsQuery(raw, project, phrenPath),
541
570
  ].filter(Boolean);
571
+ // For short queries, add a prefix-expanded variant to catch partial matches
572
+ const clauses = buildFtsClauses(raw, project, phrenPath);
573
+ if (clauses.length <= 2) {
574
+ const prefixParts = clauses
575
+ .map(c => c.replace(/^"(.*)"$/, "$1"))
576
+ .filter(t => t.length >= 3)
577
+ .map(t => `"${t}"*`);
578
+ if (prefixParts.length > 0) {
579
+ variants.push(prefixParts.join(" OR "));
580
+ }
581
+ }
542
582
  return [...new Set(variants)];
543
583
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.27",
3
+ "version": "0.0.32",
4
4
  "description": "Knowledge layer for AI agents. Phren learns and recalls.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,32 +10,33 @@
10
10
  "mcp/dist",
11
11
  "icon.svg",
12
12
  "starter",
13
- "skills"
13
+ "skills",
14
+ "scripts/preuninstall.mjs"
14
15
  ],
15
16
  "dependencies": {
16
17
  "@modelcontextprotocol/sdk": "^1.27.1",
17
18
  "chalk": "^5.6.2",
18
- "esbuild": "^0.27.4",
19
19
  "glob": "^13.0.6",
20
20
  "graphology": "^0.26.0",
21
21
  "graphology-layout-forceatlas2": "^0.10.1",
22
- "inquirer": "^12.10.0",
22
+ "inquirer": "^13.3.2",
23
23
  "js-yaml": "^4.1.1",
24
24
  "sigma": "^3.0.2",
25
25
  "sql.js-fts5": "^1.4.0",
26
26
  "zod": "^4.3.6"
27
27
  },
28
28
  "devDependencies": {
29
+ "esbuild": "^0.27.4",
29
30
  "@playwright/test": "^1.58.2",
30
31
  "@types/js-yaml": "^4.0.9",
31
- "@types/node": "^25.3.5",
32
- "@typescript-eslint/eslint-plugin": "^8.56.1",
33
- "@typescript-eslint/parser": "^8.56.1",
34
- "@vitest/coverage-v8": "^4.0.18",
35
- "eslint": "^10.0.3",
32
+ "@types/node": "^25.5.0",
33
+ "@typescript-eslint/eslint-plugin": "^8.57.1",
34
+ "@typescript-eslint/parser": "^8.57.1",
35
+ "@vitest/coverage-v8": "^4.1.0",
36
+ "eslint": "^10.1.0",
36
37
  "tsx": "^4.21.0",
37
38
  "typescript": "^5.9.3",
38
- "vitest": "^4.0.18"
39
+ "vitest": "^4.1.0"
39
40
  },
40
41
  "scripts": {
41
42
  "build": "node scripts/build.mjs",
@@ -49,6 +50,7 @@
49
50
  "bench": "tsx mcp/bench/locomo-runner.ts --sessions 3",
50
51
  "bench:retrieval": "tsx scripts/bench-retrieval-modes.ts",
51
52
  "bench:retrieval:synthetic": "tsx scripts/bench-retrieval-synthetic.ts",
53
+ "preuninstall": "node scripts/preuninstall.mjs",
52
54
  "prepublishOnly": "npm run build && npm test"
53
55
  },
54
56
  "engines": {
@@ -67,5 +69,9 @@
67
69
  "type": "git",
68
70
  "url": "git+https://github.com/alaarab/phren.git"
69
71
  },
70
- "homepage": "https://github.com/alaarab/phren#readme"
72
+ "homepage": "https://github.com/alaarab/phren#readme",
73
+ "overrides": {
74
+ "flatted": "^3.4.2",
75
+ "undici": "^7.10.0"
76
+ }
71
77
  }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * npm preuninstall hook — clean up hooks and MCP server entries from agent
5
+ * config files so they don't become orphaned when the package is removed
6
+ * via `npm uninstall -g @phren/cli` (bypassing `phren uninstall`).
7
+ *
8
+ * Only removes config/hooks — does NOT touch ~/.phren data.
9
+ */
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
16
+ const homePath = (...parts) => path.join(home, ...parts);
17
+
18
+ /** Read JSON, apply mutator, write back. */
19
+ function patchJson(filePath, mutator) {
20
+ if (!fs.existsSync(filePath)) return;
21
+ try {
22
+ const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
23
+ mutator(data);
24
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
25
+ } catch {
26
+ // best-effort — don't fail the uninstall
27
+ }
28
+ }
29
+
30
+ /** Check if a hook command string belongs to phren. */
31
+ function isPhrenCommand(cmd) {
32
+ return (
33
+ cmd.includes("@phren/cli") ||
34
+ cmd.includes("phren/cli") ||
35
+ /\bhook-(prompt|stop|session-start|tool|context)\b/.test(cmd)
36
+ );
37
+ }
38
+
39
+ // ── Claude Code settings.json: remove hooks + MCP server ──
40
+ const claudeSettings = homePath(".claude", "settings.json");
41
+ patchJson(claudeSettings, (data) => {
42
+ // Remove MCP server
43
+ if (data.mcpServers?.phren) delete data.mcpServers.phren;
44
+
45
+ // Remove phren hooks
46
+ if (data.hooks && typeof data.hooks === "object") {
47
+ for (const event of ["UserPromptSubmit", "Stop", "SessionStart", "PostToolUse"]) {
48
+ const entries = data.hooks[event];
49
+ if (!Array.isArray(entries)) continue;
50
+ data.hooks[event] = entries.filter(
51
+ (entry) => !entry.hooks?.some((h) => typeof h.command === "string" && isPhrenCommand(h.command))
52
+ );
53
+ if (data.hooks[event].length === 0) delete data.hooks[event];
54
+ }
55
+ if (Object.keys(data.hooks).length === 0) delete data.hooks;
56
+ }
57
+ });
58
+
59
+ // ── Claude Code ~/.claude.json: remove MCP server ──
60
+ const claudeJson = homePath(".claude.json");
61
+ patchJson(claudeJson, (data) => {
62
+ if (data.mcpServers?.phren) delete data.mcpServers.phren;
63
+ });
64
+
65
+ // ── VS Code MCP configs ──
66
+ const vscodeMcpCandidates = [
67
+ homePath(".vscode-server", "data", "User", "mcp.json"),
68
+ homePath("AppData", "Roaming", "Code", "User", "mcp.json"),
69
+ homePath(".config", "Code", "User", "mcp.json"),
70
+ ];
71
+ for (const mcpFile of vscodeMcpCandidates) {
72
+ patchJson(mcpFile, (data) => {
73
+ // servers array format
74
+ if (Array.isArray(data.servers)) {
75
+ data.servers = data.servers.filter((s) => s.name !== "phren");
76
+ }
77
+ // mcpServers object format
78
+ if (data.mcpServers?.phren) delete data.mcpServers.phren;
79
+ });
80
+ }
81
+
82
+ // ── Cursor MCP config ──
83
+ const cursorMcpCandidates = [
84
+ homePath(".cursor", "mcp.json"),
85
+ ];
86
+ for (const mcpFile of cursorMcpCandidates) {
87
+ patchJson(mcpFile, (data) => {
88
+ if (data.mcpServers?.phren) delete data.mcpServers.phren;
89
+ });
90
+ }
91
+
92
+ // ── Cursor hooks ──
93
+ const cursorHooks = homePath(".cursor", "hooks.json");
94
+ patchJson(cursorHooks, (data) => {
95
+ for (const key of ["sessionStart", "beforeSubmitPrompt", "stop"]) {
96
+ if (data[key]?.command && typeof data[key].command === "string" && isPhrenCommand(data[key].command)) {
97
+ delete data[key];
98
+ }
99
+ }
100
+ });
101
+
102
+ // ── Copilot hooks ──
103
+ const copilotHooks = homePath(".github", "hooks", "phren.json");
104
+ if (fs.existsSync(copilotHooks)) {
105
+ try { fs.unlinkSync(copilotHooks); } catch { /* best-effort */ }
106
+ }
107
+
108
+ // ── Codex MCP config (TOML + JSON) ──
109
+ const codexToml = homePath(".codex", "config.toml");
110
+ if (fs.existsSync(codexToml)) {
111
+ try {
112
+ const content = fs.readFileSync(codexToml, "utf8");
113
+ // Remove [mcp_servers.phren] section
114
+ const cleaned = content.replace(/\[mcp_servers\.phren\][^\[]*/, "").trim();
115
+ if (cleaned !== content.trim()) fs.writeFileSync(codexToml, cleaned + "\n");
116
+ } catch { /* best-effort */ }
117
+ }
118
+
119
+ // ── Session wrapper scripts ──
120
+ const localBinDir = homePath(".local", "bin");
121
+ for (const tool of ["copilot", "cursor", "codex"]) {
122
+ const wrapperPath = path.join(localBinDir, tool);
123
+ try {
124
+ if (fs.existsSync(wrapperPath)) {
125
+ const content = fs.readFileSync(wrapperPath, "utf8");
126
+ if (content.includes("PHREN_PATH") && content.includes("phren")) {
127
+ fs.unlinkSync(wrapperPath);
128
+ }
129
+ }
130
+ } catch { /* best-effort */ }
131
+ }
132
+
133
+ // ── Machine context file ──
134
+ const contextFile = homePath(".phren-context.md");
135
+ if (fs.existsSync(contextFile)) {
136
+ try { fs.unlinkSync(contextFile); } catch { /* best-effort */ }
137
+ }
138
+
139
+ console.log("phren: cleaned up hooks and MCP config from agent settings.");
@@ -19,9 +19,10 @@ These skills are available as a full set via phren, or individually from the Cla
19
19
  | Skill | What it does |
20
20
  |-------|-------------|
21
21
  | `/phren-sync` | Pull phren to a new machine or push config changes back to the repo |
22
- | `/phren-init` | Scaffold a new project with summary, CLAUDE.md, task |
22
+ | `/phren-init` | Scaffold a new project with summary, CLAUDE.md, tasks |
23
23
  | `/phren-discover` | Research what's missing in a project and surface gaps and opportunities |
24
24
  | `/phren-consolidate` | Find patterns across all project FINDINGS.md files |
25
+ | `/phren-profiles` | Manages machine-to-profile mapping |
25
26
 
26
27
  ### Your own skills
27
28
 
@@ -48,7 +49,7 @@ In shared mode, skills and project config live in `~/.phren` (or wherever `PHREN
48
49
  If you're using `phren init --mode project-local`, the root is `<repo>/.phren` instead. Project-local mode does not use profiles, machine mappings, or global hooks.
49
50
 
50
51
  - `~/.phren/global/`: skills and config that apply everywhere
51
- - `~/.phren/<project>/`: per-project CLAUDE.md, skills, task, findings
52
+ - `~/.phren/<project>/`: per-project CLAUDE.md, skills, tasks, findings
52
53
  - `~/.phren/profiles/`: YAML files mapping project sets to machine roles
53
54
  - `~/.phren/machines.yaml`: maps machine hostnames to profiles
54
55