@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,11 +1,11 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { PhrenError, phrenErr, phrenOk } from "./phren-core.js";
3
+ import { PhrenError, phrenErr, phrenOk } from "../phren-core.js";
4
4
  // Phren lifecycle comment prefix. No backward compat.
5
5
  const LIFECYCLE_PREFIX = "phren";
6
- import { withFileLock } from "./shared-governance.js";
7
- import { isValidProjectName, safeProjectPath } from "./utils.js";
8
- import { isArchiveEnd, isArchiveStart, parseCreatedDate as parseCreatedDateMeta, parseStatusField, parseStatus, parseSupersession, parseContradiction, parseFindingId as parseFindingIdMeta, stripLifecycleMetadata, stripRelationMetadata, normalizeFindingText, } from "./content-metadata.js";
6
+ import { withFileLock } from "../shared/governance.js";
7
+ import { isValidProjectName, safeProjectPath } from "../utils.js";
8
+ import { isArchiveEnd, isArchiveStart, parseCreatedDate as parseCreatedDateMeta, parseStatusField, parseStatus, parseSupersession, parseContradiction, parseFindingId as parseFindingIdMeta, stripLifecycleMetadata, stripRelationMetadata, normalizeFindingText, } from "../content/metadata.js";
9
9
  export const FINDING_TYPE_DECAY = {
10
10
  'pattern': { maxAgeDays: 365, decayMultiplier: 1.0 }, // Slow decay, long-lived
11
11
  'decision': { maxAgeDays: Infinity, decayMultiplier: 1.0 }, // Never decays
@@ -219,7 +219,9 @@ export function supersedeFinding(phrenPath, project, findingText, supersededBy)
219
219
  const today = new Date().toISOString().slice(0, 10);
220
220
  lines[matched.data.index] = applyLifecycle(lines[matched.data.index], { status: "superseded", status_updated: today, status_reason: "superseded_by", status_ref: ref }, today, { supersededBy: ref });
221
221
  const normalized = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
222
- fs.writeFileSync(findingsPath, normalized);
222
+ const tmpPath = findingsPath + ".tmp." + process.pid;
223
+ fs.writeFileSync(tmpPath, normalized);
224
+ fs.renameSync(tmpPath, findingsPath);
223
225
  return phrenOk({ finding: matched.data.text, superseded_by: ref, status: "superseded" });
224
226
  });
225
227
  }
@@ -239,7 +241,9 @@ export function retractFinding(phrenPath, project, findingText, reason) {
239
241
  const today = new Date().toISOString().slice(0, 10);
240
242
  lines[matched.data.index] = applyLifecycle(lines[matched.data.index], { status: "retracted", status_updated: today, status_reason: reasonText }, today);
241
243
  const normalized = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
242
- fs.writeFileSync(findingsPath, normalized);
244
+ const tmpPath = findingsPath + ".tmp." + process.pid;
245
+ fs.writeFileSync(tmpPath, normalized);
246
+ fs.renameSync(tmpPath, findingsPath);
243
247
  return phrenOk({ finding: matched.data.text, reason: reasonText, status: "retracted" });
244
248
  });
245
249
  }
@@ -289,7 +293,9 @@ export function resolveFindingContradiction(phrenPath, project, findingA, findin
289
293
  statusB = "retracted";
290
294
  }
291
295
  const normalized = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
292
- fs.writeFileSync(findingsPath, normalized);
296
+ const tmpPath = findingsPath + ".tmp." + process.pid;
297
+ fs.writeFileSync(tmpPath, normalized);
298
+ fs.renameSync(tmpPath, findingsPath);
293
299
  return phrenOk({
294
300
  resolution,
295
301
  finding_a: { text: matchedA.data.text, status: statusA },
@@ -0,0 +1,30 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { debugLog } from "../shared.js";
4
+ import { errorMessage } from "../utils.js";
5
+ const MAX_LOG_LINES = 1000;
6
+ export function recordRetrieval(phrenPath, file, section) {
7
+ const dir = path.join(phrenPath, ".runtime");
8
+ let logPath;
9
+ try {
10
+ fs.mkdirSync(dir, { recursive: true });
11
+ logPath = path.join(dir, "retrieval-log.jsonl");
12
+ const entry = { file, section, retrievedAt: new Date().toISOString() };
13
+ fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
14
+ }
15
+ catch (err) {
16
+ debugLog(`recordRetrieval write failed: ${errorMessage(err)}`);
17
+ return;
18
+ }
19
+ try {
20
+ const stat = fs.statSync(logPath);
21
+ if (stat.size > 500_000) {
22
+ const content = fs.readFileSync(logPath, "utf8");
23
+ const lines = content.split("\n").filter(Boolean);
24
+ fs.writeFileSync(logPath, lines.slice(-MAX_LOG_LINES).join("\n") + "\n");
25
+ }
26
+ }
27
+ catch (err) {
28
+ debugLog(`recordRetrieval rotation failed: ${errorMessage(err)}`);
29
+ }
30
+ }
@@ -1,7 +1,8 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { debugLog } from "./shared.js";
4
- import { errorMessage } from "./utils.js";
3
+ import { debugLog } from "../shared.js";
4
+ import { errorMessage } from "../utils.js";
5
+ import { logger } from "../logger.js";
5
6
  // Acquire the file lock, returning true on success or throwing on timeout.
6
7
  function acquireFileLock(lockPath) {
7
8
  const maxWait = Number.parseInt(process.env.PHREN_FILE_LOCK_MAX_WAIT_MS || "5000", 10) || 5000;
@@ -19,8 +20,7 @@ function acquireFileLock(lockPath) {
19
20
  break;
20
21
  }
21
22
  catch (err) {
22
- if ((process.env.PHREN_DEBUG))
23
- process.stderr.write(`[phren] acquireFileLock lockWrite: ${errorMessage(err)}\n`);
23
+ logger.debug("acquireFileLock", `lockWrite: ${errorMessage(err)}`);
24
24
  try {
25
25
  const stat = fs.statSync(lockPath);
26
26
  if (Date.now() - stat.mtimeMs > staleThreshold) {
@@ -40,7 +40,14 @@ function acquireFileLock(lockPath) {
40
40
  }
41
41
  }
42
42
  else {
43
- ownerDead = true;
43
+ try {
44
+ const result = require('child_process').spawnSync('tasklist', ['/FI', `PID eq ${lockPid}`, '/NH'], { encoding: 'utf8', timeout: 2000 });
45
+ if (result.stdout && result.stdout.includes(String(lockPid)))
46
+ ownerDead = false;
47
+ }
48
+ catch {
49
+ ownerDead = true;
50
+ }
44
51
  }
45
52
  }
46
53
  }
@@ -54,8 +61,7 @@ function acquireFileLock(lockPath) {
54
61
  }
55
62
  }
56
63
  catch (statErr) {
57
- if ((process.env.PHREN_DEBUG))
58
- process.stderr.write(`[phren] acquireFileLock staleStat: ${statErr instanceof Error ? statErr.message : String(statErr)}\n`);
64
+ logger.debug("acquireFileLock", `staleStat: ${statErr instanceof Error ? statErr.message : String(statErr)}`);
59
65
  sleep(pollInterval);
60
66
  waited += pollInterval;
61
67
  continue;
@@ -75,8 +81,7 @@ function releaseFileLock(lockPath) {
75
81
  fs.unlinkSync(lockPath);
76
82
  }
77
83
  catch (err) {
78
- if ((process.env.PHREN_DEBUG))
79
- process.stderr.write(`[phren] releaseFileLock: ${errorMessage(err)}\n`);
84
+ logger.debug("releaseFileLock", `${errorMessage(err)}`);
80
85
  }
81
86
  }
82
87
  // Q10: withFileLock now accepts both sync and async callbacks.
@@ -1,14 +1,27 @@
1
1
  import * as crypto from "crypto";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { appendAuditLog, debugLog, getProjectDirs, isRecord, runtimeHealthFile, withDefaults, phrenErr, PhrenError, phrenOk, resolveFindingsPath } from "./shared.js";
5
- import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "./shared-governance.js";
6
- import { errorMessage, isValidProjectName, safeProjectPath } from "./utils.js";
7
- import { readProjectConfig } from "./project-config.js";
8
- import { getActiveProfileDefaults } from "./profile-store.js";
9
- import { runCustomHooks } from "./hooks.js";
10
- import { METADATA_REGEX, isCitationLine, isArchiveStart as isArchiveStartMeta, isArchiveEnd as isArchiveEndMeta, stripLifecycleMetadata as stripLifecycleMetadataMeta, } from "./content-metadata.js";
4
+ import { appendAuditLog, debugLog, getProjectDirs, isRecord, runtimeHealthFile, withDefaults, phrenErr, PhrenError, phrenOk, resolveFindingsPath } from "../shared.js";
5
+ import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "../shared/governance.js";
6
+ import { errorMessage, isValidProjectName, safeProjectPath } from "../utils.js";
7
+ import { readProjectConfig } from "../project-config.js";
8
+ import { getActiveProfileDefaults } from "../profile-store.js";
9
+ import { runCustomHooks } from "../hooks.js";
10
+ import { METADATA_REGEX, isCitationLine, isArchiveStart as isArchiveStartMeta, isArchiveEnd as isArchiveEndMeta, stripLifecycleMetadata as stripLifecycleMetadataMeta, } from "../content/metadata.js";
11
+ /** @internal Exported for tests. */
11
12
  export const MAX_QUEUE_ENTRY_LENGTH = 500;
13
+ export function buildSyncStatus(opts) {
14
+ return {
15
+ ...(opts.pullAt !== undefined ? { lastPullAt: opts.pullAt } : {}),
16
+ ...(opts.pullStatus !== undefined ? { lastPullStatus: opts.pullStatus } : {}),
17
+ ...(opts.pullDetail !== undefined ? { lastPullDetail: opts.pullDetail } : {}),
18
+ ...(opts.successfulPullAt !== undefined ? { lastSuccessfulPullAt: opts.successfulPullAt } : {}),
19
+ lastPushAt: opts.now,
20
+ lastPushStatus: opts.pushStatus,
21
+ ...(opts.pushDetail !== undefined ? { lastPushDetail: opts.pushDetail } : {}),
22
+ ...(opts.unsyncedCommits !== undefined ? { unsyncedCommits: opts.unsyncedCommits } : {}),
23
+ };
24
+ }
12
25
  export const GOVERNANCE_SCHEMA_VERSION = 1;
13
26
  const DEFAULT_POLICY = {
14
27
  schemaVersion: GOVERNANCE_SCHEMA_VERSION,
@@ -641,7 +654,7 @@ export function pruneDeadMemories(phrenPath, project, dryRun) {
641
654
  const file = resolveFindingsPath(dir);
642
655
  if (!file)
643
656
  continue;
644
- // Q23: wrap read-modify-write in per-file lock to prevent races with concurrent finding writers
657
+ // Q23: see docs/decisions/Q23-per-file-lock-concurrent-writers.md
645
658
  withFileLock(file, () => {
646
659
  const lines = fs.readFileSync(file, "utf8").split("\n");
647
660
  let currentDate = null;
@@ -723,13 +736,11 @@ export function consolidateProjectFindings(phrenPath, project, dryRun) {
723
736
  const file = resolveFindingsPath(path.join(phrenPath, project));
724
737
  if (!file)
725
738
  return phrenErr(`No FINDINGS.md found for "${project}".`, PhrenError.FILE_NOT_FOUND);
726
- // Q23: wrap entire read-modify-write in per-file lock to prevent races with concurrent finding writers
739
+ // Q23: see docs/decisions/Q23-per-file-lock-concurrent-writers.md
727
740
  const result = withFileLock(file, () => {
728
741
  const raw = fs.readFileSync(file, "utf8");
729
742
  const lines = raw.split("\n");
730
- // Q12: Separate the file into "active" lines and verbatim archive/details blocks.
731
- // Archive blocks (<!-- phren:archive:start/end --> and <details>...</details>) are
732
- // collected verbatim and appended unchanged after the consolidated active section.
743
+ // Q12: see docs/decisions/Q12-active-vs-archive-separation.md
733
744
  const archiveBlocks = [];
734
745
  const activeLines = [];
735
746
  let inArchive = false;
@@ -18,9 +18,9 @@
18
18
  */
19
19
  import * as fs from "fs";
20
20
  import * as path from "path";
21
- import { debugLog } from "./shared.js";
22
- import { errorMessage } from "./utils.js";
23
- import { readProjectConfig } from "./project-config.js";
21
+ import { debugLog } from "../shared.js";
22
+ import { errorMessage } from "../utils.js";
23
+ import { readProjectConfig } from "../project-config.js";
24
24
  function configDir(phrenPath) {
25
25
  return path.join(phrenPath, ".config");
26
26
  }
@@ -104,7 +104,7 @@ function rolePermits(role, action) {
104
104
  *
105
105
  * Returns `{ allowed: true }` when permitted, `{ allowed: false, reason }` when denied.
106
106
  */
107
- export function checkPermission(phrenPath, action, project) {
107
+ function checkPermission(phrenPath, action, project) {
108
108
  const actor = (process.env.PHREN_ACTOR ?? "").trim() || null;
109
109
  const globalAc = readGlobalAccessControl(phrenPath);
110
110
  const projectAccess = project
@@ -1,9 +1,11 @@
1
1
  import * as crypto from "crypto";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { appendAuditLog, debugLog, isRecord, memoryScoresFile, memoryUsageLogFile, runtimeFile } from "./shared.js";
5
- import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "./shared-governance.js";
6
- import { errorMessage } from "./utils.js";
4
+ import { appendAuditLog, debugLog, isRecord, memoryScoresFile, memoryUsageLogFile, runtimeFile } from "../shared.js";
5
+ import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "../shared/governance.js";
6
+ import { errorMessage } from "../utils.js";
7
+ import { logger } from "../logger.js";
8
+ const MAX_LOG_LINES = 1000;
7
9
  const GOVERNANCE_SCHEMA_VERSION = 1;
8
10
  const DEFAULT_MEMORY_SCORES_FILE = {
9
11
  schemaVersion: GOVERNANCE_SCHEMA_VERSION,
@@ -114,8 +116,7 @@ function readScoreJournal(phrenPath) {
114
116
  return JSON.parse(line);
115
117
  }
116
118
  catch (err) {
117
- if ((process.env.PHREN_DEBUG))
118
- process.stderr.write(`[phren] readScoreJournal parseLine: ${errorMessage(err)}\n`);
119
+ logger.debug("scores", `readScoreJournal parseLine: ${errorMessage(err)}`);
119
120
  return null;
120
121
  }
121
122
  })
@@ -147,8 +148,7 @@ function claimScoreJournal(phrenPath) {
147
148
  return JSON.parse(line);
148
149
  }
149
150
  catch (err) {
150
- if ((process.env.PHREN_DEBUG))
151
- process.stderr.write(`[phren] claimScoreJournal parseLine: ${errorMessage(err)}\n`);
151
+ logger.debug("scores", `claimScoreJournal parseLine: ${errorMessage(err)}`);
152
152
  return null;
153
153
  }
154
154
  })
@@ -163,8 +163,7 @@ function claimScoreJournal(phrenPath) {
163
163
  fs.unlinkSync(claimedFile);
164
164
  }
165
165
  catch (err) {
166
- if ((process.env.PHREN_DEBUG))
167
- process.stderr.write(`[phren] claimScoreJournal unlinkClaim: ${errorMessage(err)}\n`);
166
+ logger.debug("scores", `claimScoreJournal unlinkClaim: ${errorMessage(err)}`);
168
167
  }
169
168
  }
170
169
  }
@@ -257,7 +256,7 @@ export function recordInjection(phrenPath, key, sessionId) {
257
256
  if (stat.size > 1_000_000) {
258
257
  const content = fs.readFileSync(logFile, "utf8");
259
258
  const lines = content.split("\n");
260
- fs.writeFileSync(logFile, lines.slice(-500).join("\n"));
259
+ fs.writeFileSync(logFile, lines.slice(-MAX_LOG_LINES).join("\n"));
261
260
  }
262
261
  }
263
262
  catch (err) {
@@ -276,7 +275,7 @@ export function recordFeedback(phrenPath, key, feedback, sessionId) {
276
275
  appendAuditLog(phrenPath, "memory_feedback", `key=${key} feedback=${feedback}`);
277
276
  // When feedback is "helpful", mark correlated query entries for future boost
278
277
  if (feedback === "helpful" && sessionId) {
279
- import("./query-correlation.js").then(({ markCorrelationsHelpful: markHelpful }) => {
278
+ import("../query-correlation.js").then(({ markCorrelationsHelpful: markHelpful }) => {
280
279
  const colonIdx = key.indexOf(":");
281
280
  const docKey = colonIdx >= 0 ? key.slice(0, colonIdx) : key;
282
281
  markHelpful(phrenPath, sessionId, docKey);
package/mcp/dist/hooks.js CHANGED
@@ -9,6 +9,8 @@ import { EXEC_TIMEOUT_QUICK_MS, PhrenError, debugLog, runtimeFile, homePath, ins
9
9
  import { errorMessage } from "./utils.js";
10
10
  import { hookConfigPath } from "./provider-adapters.js";
11
11
  import { PACKAGE_SPEC } from "./package-metadata.js";
12
+ import { logger } from "./logger.js";
13
+ import { withFileLock } from "./shared/governance.js";
12
14
  export function commandExists(cmd) {
13
15
  try {
14
16
  const whichCmd = process.platform === "win32" ? "where.exe" : "which";
@@ -67,10 +69,6 @@ function phrenPackageSpec() {
67
69
  export function shellEscape(s) {
68
70
  return "'" + s.replace(/'/g, "'\\''") + "'";
69
71
  }
70
- /** @deprecated Use shellEscape instead */
71
- function shellSingleQuote(value) {
72
- return shellEscape(value);
73
- }
74
72
  function buildPackageLifecycleCommands() {
75
73
  const packageSpec = phrenPackageSpec();
76
74
  return {
@@ -84,10 +82,10 @@ export function buildLifecycleCommands(phrenPath) {
84
82
  const entry = resolveCliEntryScript();
85
83
  const isWindows = process.platform === "win32";
86
84
  const escapedPhren = phrenPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
87
- const quotedPhren = shellSingleQuote(phrenPath);
85
+ const quotedPhren = shellEscape(phrenPath);
88
86
  if (entry) {
89
87
  const escapedEntry = entry.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
90
- const quotedEntry = shellSingleQuote(entry);
88
+ const quotedEntry = shellEscape(entry);
91
89
  if (isWindows) {
92
90
  return {
93
91
  sessionStart: `set "PHREN_PATH=${escapedPhren}" && node "${escapedEntry}" hook-session-start`,
@@ -126,7 +124,7 @@ function withHookToolEnv(command, tool) {
126
124
  if (process.platform === "win32") {
127
125
  return `set "PHREN_HOOK_TOOL=${tool}" && ${command}`;
128
126
  }
129
- return `PHREN_HOOK_TOOL=${shellSingleQuote(tool)} ${command}`;
127
+ return `PHREN_HOOK_TOOL=${shellEscape(tool)} ${command}`;
130
128
  }
131
129
  function withHookToolLifecycleCommands(lifecycle, tool) {
132
130
  return {
@@ -153,10 +151,10 @@ function installSessionWrapper(tool, phrenPath) {
153
151
  const content = `#!/bin/sh
154
152
  set -u
155
153
 
156
- REAL_BIN=${shellSingleQuote(realBinary)}
157
- DEFAULT_PHREN_PATH=${shellSingleQuote(phrenPath)}
154
+ REAL_BIN=${shellEscape(realBinary)}
155
+ DEFAULT_PHREN_PATH=${shellEscape(phrenPath)}
158
156
  PHREN_PATH="\${PHREN_PATH:-$DEFAULT_PHREN_PATH}"
159
- ENTRY_SCRIPT=${shellSingleQuote(entry || "")}
157
+ ENTRY_SCRIPT=${shellEscape(entry || "")}
160
158
  export PHREN_HOOK_TOOL="${tool}"
161
159
 
162
160
  if [ ! -x "$REAL_BIN" ]; then
@@ -238,7 +236,14 @@ function cachedReadInstallPrefsJson(phrenPath) {
238
236
  if (cached && cached.mtimeMs === mtimeMs) {
239
237
  return cached.parsed;
240
238
  }
241
- const parsed = JSON.parse(fs.readFileSync(prefsPath, "utf8"));
239
+ let parsed;
240
+ try {
241
+ parsed = JSON.parse(fs.readFileSync(prefsPath, "utf8"));
242
+ }
243
+ catch {
244
+ _installPrefsJsonCache.delete(prefsPath);
245
+ return null;
246
+ }
242
247
  _installPrefsJsonCache.set(prefsPath, { mtimeMs, parsed });
243
248
  return parsed;
244
249
  }
@@ -274,6 +279,7 @@ export const HOOK_EVENT_VALUES = [
274
279
  "post-session-end", "post-consolidate",
275
280
  ];
276
281
  const VALID_HOOK_EVENTS = new Set(HOOK_EVENT_VALUES);
282
+ const MAX_HOOK_COMMAND_LENGTH = 1000;
277
283
  /** Return the target (URL or shell command) for display or matching. */
278
284
  export function getHookTarget(h) {
279
285
  return "webhook" in h ? h.webhook : h.command;
@@ -282,8 +288,8 @@ export function validateCustomHookCommand(command) {
282
288
  const trimmed = command.trim();
283
289
  if (!trimmed)
284
290
  return "Command cannot be empty.";
285
- if (trimmed.length > 1000)
286
- return "Command too long (max 1000 characters).";
291
+ if (trimmed.length > MAX_HOOK_COMMAND_LENGTH)
292
+ return `Command too long (max ${MAX_HOOK_COMMAND_LENGTH} characters).`;
287
293
  if (/[`$(){}&|;<>\n\r#]/.test(trimmed)) {
288
294
  return "Command contains disallowed shell characters: ` $ ( ) { } & | ; < > # \\n \\r";
289
295
  }
@@ -399,9 +405,7 @@ async function validateAndResolveWebhook(webhook) {
399
405
  }
400
406
  catch (err) {
401
407
  debugLog(`validateAndResolveWebhook lookup failed for ${parsed.hostname}: ${errorMessage(err)}`);
402
- // DNS resolution failed; allow the fetch to proceed with the original URL
403
- // (fetch will do its own resolution and may fail with a network error)
404
- return { resolvedUrl: webhook, host: parsed.host };
408
+ return { error: `webhook hostname "${parsed.hostname}" could not be resolved: ${errorMessage(err)}` };
405
409
  }
406
410
  }
407
411
  const DEFAULT_CUSTOM_HOOK_TIMEOUT = 5000;
@@ -430,18 +434,24 @@ export function readCustomHooks(phrenPath) {
430
434
  function appendHookErrorLog(phrenPath, event, message) {
431
435
  const logPath = runtimeFile(phrenPath, "hook-errors.log");
432
436
  const line = `[${new Date().toISOString()}] [${event}] ${message}\n`;
433
- fs.appendFileSync(logPath, line);
434
437
  try {
435
- const stat = fs.statSync(logPath);
436
- if (stat.size > 200_000) {
437
- const content = fs.readFileSync(logPath, "utf-8");
438
- const lines = content.split("\n").filter(Boolean);
439
- atomicWriteText(logPath, lines.slice(-HOOK_ERROR_LOG_MAX_LINES).join("\n") + "\n");
440
- }
438
+ withFileLock(logPath, () => {
439
+ fs.appendFileSync(logPath, line);
440
+ try {
441
+ const stat = fs.statSync(logPath);
442
+ if (stat.size > 200_000) {
443
+ const content = fs.readFileSync(logPath, "utf-8");
444
+ const lines = content.split("\n").filter(Boolean);
445
+ atomicWriteText(logPath, lines.slice(-HOOK_ERROR_LOG_MAX_LINES).join("\n") + "\n");
446
+ }
447
+ }
448
+ catch (err) {
449
+ logger.debug("appendHookErrorLog rotate", errorMessage(err));
450
+ }
451
+ });
441
452
  }
442
453
  catch (err) {
443
- if (process.env.PHREN_DEBUG)
444
- process.stderr.write(`[phren] appendHookErrorLog rotate: ${errorMessage(err)}\n`);
454
+ logger.debug("appendHookErrorLog lock", errorMessage(err));
445
455
  }
446
456
  }
447
457
  export function runCustomHooks(phrenPath, event, env = {}) {
@@ -488,8 +498,7 @@ export function runCustomHooks(phrenPath, event, env = {}) {
488
498
  appendHookErrorLog(phrenPath, event, message);
489
499
  }
490
500
  catch (logErr) {
491
- if (process.env.PHREN_DEBUG)
492
- process.stderr.write(`[phren] runCustomHooks webhookErrorLog: ${errorMessage(logErr)}\n`);
501
+ logger.debug("runCustomHooks webhookErrorLog", errorMessage(logErr));
493
502
  }
494
503
  });
495
504
  continue;
@@ -503,12 +512,22 @@ export function runCustomHooks(phrenPath, event, env = {}) {
503
512
  continue;
504
513
  }
505
514
  const shellArgs = isWindows ? ["/c", hook.command] : ["-c", hook.command];
515
+ // On Windows, cmd /c expands %VAR% in the command string.
516
+ // Sanitize env values to prevent shell metacharacter injection.
517
+ const mergedEnv = { ...process.env, PHREN_PATH: phrenPath, PHREN_HOOK_EVENT: event, ...env };
518
+ if (isWindows) {
519
+ for (const [key, val] of Object.entries(mergedEnv)) {
520
+ if (typeof val === "string") {
521
+ mergedEnv[key] = val.replace(/[&|<>^%]/g, "");
522
+ }
523
+ }
524
+ }
506
525
  try {
507
526
  execFileSync(shellCmd, shellArgs, {
508
527
  cwd: phrenPath,
509
528
  encoding: "utf8",
510
529
  timeout: hook.timeout ?? DEFAULT_CUSTOM_HOOK_TIMEOUT,
511
- env: { ...process.env, PHREN_PATH: phrenPath, PHREN_HOOK_EVENT: event, ...env },
530
+ env: mergedEnv,
512
531
  stdio: ["ignore", "ignore", "pipe"],
513
532
  });
514
533
  }
@@ -520,8 +539,7 @@ export function runCustomHooks(phrenPath, event, env = {}) {
520
539
  appendHookErrorLog(phrenPath, event, errorMessage(err));
521
540
  }
522
541
  catch (logErr) {
523
- if (process.env.PHREN_DEBUG)
524
- process.stderr.write(`[phren] runCustomHooks hookErrorLog: ${errorMessage(logErr)}\n`);
542
+ logger.debug("runCustomHooks hookErrorLog", errorMessage(logErr));
525
543
  }
526
544
  }
527
545
  }
@@ -556,7 +574,7 @@ export function configureAllHooks(phrenPath, options = {}) {
556
574
  configured.push("Copilot CLI");
557
575
  }
558
576
  catch (err) {
559
- debugLog(`configureAllHooks: copilot failed: ${errorMessage(err)}`);
577
+ console.warn(`configureAllHooks: copilot hook config failed: ${errorMessage(err)}`);
560
578
  }
561
579
  if (isToolHookEnabled(phrenPath, "copilot"))
562
580
  installSessionWrapper("copilot", phrenPath);
@@ -572,8 +590,7 @@ export function configureAllHooks(phrenPath, options = {}) {
572
590
  existing = JSON.parse(fs.readFileSync(cursorFile, "utf8"));
573
591
  }
574
592
  catch (err) {
575
- if (process.env.PHREN_DEBUG)
576
- process.stderr.write(`[phren] configureAllHooks cursorRead: ${errorMessage(err)}\n`);
593
+ logger.debug("configureAllHooks cursorRead", errorMessage(err));
577
594
  }
578
595
  const config = {
579
596
  ...existing,
@@ -589,7 +606,7 @@ export function configureAllHooks(phrenPath, options = {}) {
589
606
  configured.push("Cursor");
590
607
  }
591
608
  catch (err) {
592
- debugLog(`configureAllHooks: cursor failed: ${errorMessage(err)}`);
609
+ console.warn(`configureAllHooks: cursor hook config failed: ${errorMessage(err)}`);
593
610
  }
594
611
  if (isToolHookEnabled(phrenPath, "cursor"))
595
612
  installSessionWrapper("cursor", phrenPath);
@@ -604,8 +621,7 @@ export function configureAllHooks(phrenPath, options = {}) {
604
621
  existing = JSON.parse(fs.readFileSync(codexFile, "utf8"));
605
622
  }
606
623
  catch (err) {
607
- if (process.env.PHREN_DEBUG)
608
- process.stderr.write(`[phren] configureAllHooks codexRead: ${errorMessage(err)}\n`);
624
+ logger.debug("configureAllHooks codexRead", errorMessage(err));
609
625
  }
610
626
  const config = {
611
627
  ...existing,
@@ -621,7 +637,7 @@ export function configureAllHooks(phrenPath, options = {}) {
621
637
  configured.push("Codex");
622
638
  }
623
639
  catch (err) {
624
- debugLog(`configureAllHooks: codex failed: ${errorMessage(err)}`);
640
+ console.warn(`configureAllHooks: codex hook config failed: ${errorMessage(err)}`);
625
641
  }
626
642
  if (isToolHookEnabled(phrenPath, "codex"))
627
643
  installSessionWrapper("codex", phrenPath);
@@ -1,5 +1,6 @@
1
1
  import * as path from "path";
2
2
  import { debugLog } from "./shared.js";
3
+ import { logger } from "./logger.js";
3
4
  function describeSqlValue(value) {
4
5
  if (value === null)
5
6
  return "null";
@@ -78,7 +79,9 @@ export function queryRows(db, sql, params) {
78
79
  return results[0].values;
79
80
  }
80
81
  catch (err) {
81
- debugLog(`queryRows failed: ${err instanceof Error ? err.message : "unknown error"}`);
82
+ const msg = err instanceof Error ? err.message : "unknown error";
83
+ logger.debug("queryRows", `DB query failed: ${msg}`);
84
+ debugLog(`queryRows failed: ${msg}`);
82
85
  return null;
83
86
  }
84
87
  }