@phren/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,172 @@
1
+ import * as fs from "fs";
2
+ import { bootstrapPhrenDotEnv } from "./phren-dotenv.js";
3
+ import { debugLog, findPhrenPath } from "./phren-paths.js";
4
+ import { governanceInstallPreferencesFile, readInstallPreferences } from "./init-preferences.js";
5
+ import { errorMessage } from "./utils.js";
6
+ import { getWorkflowPolicy } from "./governance-policy.js";
7
+ export const PROACTIVITY_LEVELS = ["high", "medium", "low"];
8
+ const DEFAULT_PROACTIVITY_LEVEL = "high";
9
+ const EXPLICIT_FINDING_SIGNAL_PATTERN = /\b(add finding|worth remembering)\b/i;
10
+ const EXPLICIT_FINDING_TAG_PATTERN = /\[(pitfall|decision|pattern|tradeoff|architecture|bug)\]/i;
11
+ const EXPLICIT_TASK_SIGNAL_PATTERN = /\b(?:add(?:\s+(?:this|that|it))?\s+(?:to\s+(?:the\s+)?)?(?:task|todo(?:\s+list)?|task(?:\s+list)?)|add\s+(?:a\s+)?task|put(?:\s+(?:this|that|it))?\s+(?:in|on)\s+(?:the\s+)?(?:task|todo(?:\s+list)?|task(?:\s+list)?))\b/i;
12
+ function parseProactivityLevel(raw) {
13
+ if (!raw)
14
+ return undefined;
15
+ const normalized = raw.trim().toLowerCase();
16
+ return PROACTIVITY_LEVELS.includes(normalized)
17
+ ? normalized
18
+ : undefined;
19
+ }
20
+ function resolveProactivityPhrenPath(explicitPhrenPath) {
21
+ return explicitPhrenPath ?? findPhrenPath();
22
+ }
23
+ function readGovernanceProactivityPreferences(explicitPhrenPath) {
24
+ const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
25
+ if (!phrenPath)
26
+ return {};
27
+ const filePath = governanceInstallPreferencesFile(phrenPath);
28
+ if (!fs.existsSync(filePath))
29
+ return {};
30
+ try {
31
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
32
+ return {
33
+ proactivity: parseProactivityLevel(parsed?.proactivity),
34
+ proactivityFindings: parseProactivityLevel(parsed?.proactivityFindings),
35
+ proactivityTask: parseProactivityLevel(parsed?.proactivityTask),
36
+ };
37
+ }
38
+ catch (err) {
39
+ debugLog(`readGovernanceProactivityPreferences: failed to parse ${filePath}: ${errorMessage(err)}`);
40
+ }
41
+ return {};
42
+ }
43
+ function getConfiguredProactivityDefault(explicitPhrenPath) {
44
+ const governancePreference = readGovernanceProactivityPreferences(explicitPhrenPath).proactivity;
45
+ if (governancePreference)
46
+ return governancePreference;
47
+ const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
48
+ if (phrenPath) {
49
+ const runtimePreference = parseProactivityLevel(readInstallPreferences(phrenPath).proactivity);
50
+ if (runtimePreference)
51
+ return runtimePreference;
52
+ }
53
+ return DEFAULT_PROACTIVITY_LEVEL;
54
+ }
55
+ /** Map findingSensitivity from workflow-policy.json to a ProactivityLevel. */
56
+ function sensitivityToProactivity(sensitivity) {
57
+ switch (sensitivity) {
58
+ case "minimal": return "low";
59
+ case "conservative": return "medium";
60
+ case "balanced": return "high";
61
+ case "aggressive": return "high";
62
+ default: return undefined;
63
+ }
64
+ }
65
+ function getWorkflowPolicySensitivityLevel(explicitPhrenPath) {
66
+ const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
67
+ if (!phrenPath)
68
+ return undefined;
69
+ try {
70
+ const policy = getWorkflowPolicy(phrenPath);
71
+ return sensitivityToProactivity(policy.findingSensitivity);
72
+ }
73
+ catch {
74
+ return undefined;
75
+ }
76
+ }
77
+ function getConfiguredProactivityLevelForFindingsDefault(explicitPhrenPath) {
78
+ const prefs = readGovernanceProactivityPreferences(explicitPhrenPath);
79
+ return prefs.proactivityFindings
80
+ ?? prefs.proactivity
81
+ ?? getWorkflowPolicySensitivityLevel(explicitPhrenPath)
82
+ ?? getConfiguredProactivityDefault(explicitPhrenPath);
83
+ }
84
+ function getConfiguredProactivityLevelForTaskDefault(explicitPhrenPath) {
85
+ const prefs = readGovernanceProactivityPreferences(explicitPhrenPath);
86
+ return prefs.proactivityTask
87
+ ?? prefs.proactivity
88
+ ?? getWorkflowPolicySensitivityLevel(explicitPhrenPath)
89
+ ?? getConfiguredProactivityDefault(explicitPhrenPath);
90
+ }
91
+ function resolveProactivityLevel(raw, fallback) {
92
+ return parseProactivityLevel(raw) ?? fallback;
93
+ }
94
+ export function getProactivityLevel(explicitPhrenPath) {
95
+ bootstrapPhrenDotEnv();
96
+ return resolveProactivityLevel(process.env.PHREN_PROACTIVITY ?? (process.env.PHREN_PROACTIVITY), getConfiguredProactivityDefault(explicitPhrenPath));
97
+ }
98
+ export function getProactivityLevelForFindings(explicitPhrenPath) {
99
+ bootstrapPhrenDotEnv();
100
+ const findingsPreference = parseProactivityLevel(process.env.PHREN_PROACTIVITY_FINDINGS ?? (process.env.PHREN_PROACTIVITY_FINDINGS));
101
+ if (findingsPreference)
102
+ return findingsPreference;
103
+ return resolveProactivityLevel(process.env.PHREN_PROACTIVITY ?? (process.env.PHREN_PROACTIVITY), getConfiguredProactivityLevelForFindingsDefault(explicitPhrenPath));
104
+ }
105
+ export function getProactivityLevelForTask(explicitPhrenPath) {
106
+ bootstrapPhrenDotEnv();
107
+ const taskPreference = parseProactivityLevel(process.env.PHREN_PROACTIVITY_TASKS ?? (process.env.PHREN_PROACTIVITY_TASKS));
108
+ if (taskPreference)
109
+ return taskPreference;
110
+ return resolveProactivityLevel(process.env.PHREN_PROACTIVITY ?? (process.env.PHREN_PROACTIVITY), getConfiguredProactivityLevelForTaskDefault(explicitPhrenPath));
111
+ }
112
+ export function hasExplicitFindingSignal(...texts) {
113
+ return texts.some((text) => {
114
+ if (!text)
115
+ return false;
116
+ return EXPLICIT_FINDING_SIGNAL_PATTERN.test(text) || EXPLICIT_FINDING_TAG_PATTERN.test(text);
117
+ });
118
+ }
119
+ export function hasExplicitTaskSignal(...texts) {
120
+ return texts.some((text) => {
121
+ if (!text)
122
+ return false;
123
+ return EXPLICIT_TASK_SIGNAL_PATTERN.test(text);
124
+ });
125
+ }
126
+ export function shouldAutoCaptureFindingsForLevel(level, ...texts) {
127
+ if (level === "high")
128
+ return true;
129
+ if (level === "low")
130
+ return false;
131
+ return hasExplicitFindingSignal(...texts);
132
+ }
133
+ export function shouldAutoCaptureTaskForLevel(level, ...texts) {
134
+ if (level === "high")
135
+ return true;
136
+ if (level === "low")
137
+ return false;
138
+ return hasExplicitTaskSignal(...texts);
139
+ }
140
+ // ── Intent-aware auto task signals ──────────────────────────────────────────
141
+ const EXECUTION_INTENT_PATTERN = /\b(?:yes\s+do\s+it|do\s+it|go\s+ahead|work\s+on\s+(?:these|this|that|it)|let(?:'|')s\s+(?:build|ship|implement|start|do|go)|approve(?:d)?|ship\s+it|merge\s+it|commit|proceed|execute|deploy|push\s+it|make\s+it\s+happen|get\s+(?:it|this|that)\s+done)\b/i;
142
+ const DISCOVERY_INTENT_PATTERN = /\b(?:brainstorm|explore|what\s+if|ideas?\b|review\s+findings|think\s+about|consider|hypothetical|might\s+we|could\s+we\s+try|what\s+are\s+(?:the|some)\s+(?:options|alternatives|possibilities)|pros?\s+(?:and|&)\s+cons?|compare|evaluate|assess|weigh)\b/i;
143
+ export function hasExecutionIntent(...texts) {
144
+ return texts.some((text) => {
145
+ if (!text)
146
+ return false;
147
+ return EXECUTION_INTENT_PATTERN.test(text);
148
+ });
149
+ }
150
+ export function hasDiscoveryIntent(...texts) {
151
+ return texts.some((text) => {
152
+ if (!text)
153
+ return false;
154
+ return DISCOVERY_INTENT_PATTERN.test(text);
155
+ });
156
+ }
157
+ const SUPPRESS_TASK_PATTERN = /\b(?:don(?:'|\u2019)t\s+(?:create|add|track|make)\s+(?:a\s+)?task|no\s+task|skip\s+(?:the\s+)?task|don(?:'|\u2019)t\s+add\s+(?:that|this|those)\s+to\s+task|do\s+not\s+add|not\s+a\s+task|just\s+(?:a\s+)?question|just\s+asking|hypothetically|just\s+curious|ignore\s+(?:this|that))\b/i;
158
+ export function hasSuppressTaskIntent(...texts) {
159
+ return texts.some((text) => {
160
+ if (!text)
161
+ return false;
162
+ return SUPPRESS_TASK_PATTERN.test(text);
163
+ });
164
+ }
165
+ const CODE_CHANGE_PATTERN = /\b(?:git\s+(?:diff|status|add|commit|log)|npm\s+(?:run|test|build)|(?:edit|fix|change|update|modify)\s+(?:the\s+)?(?:file|code|function|method|class)|diff|patch|pull\s+request|pr\s+#?\d+|changed\s+file|modified\s+file|deleted\s+file|added\s+file|renamed\s+file)\b/i;
166
+ export function hasCodeChangeContext(...texts) {
167
+ return texts.some((text) => {
168
+ if (!text)
169
+ return false;
170
+ return CODE_CHANGE_PATTERN.test(text);
171
+ });
172
+ }
@@ -0,0 +1,228 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as crypto from "crypto";
4
+ import * as yaml from "js-yaml";
5
+ import { phrenErr, PhrenError, phrenOk, forwardErr, getProjectDirs, readRootManifest, } from "./shared.js";
6
+ import { defaultMachineName, getMachineName } from "./machine-identity.js";
7
+ import { withFileLock as withFileLockRaw } from "./shared-governance.js";
8
+ import { errorMessage, isValidProjectName } from "./utils.js";
9
+ import { TASK_FILE_ALIASES } from "./data-tasks.js";
10
+ function withSafeLock(filePath, fn) {
11
+ try {
12
+ return withFileLockRaw(filePath, fn);
13
+ }
14
+ catch (err) {
15
+ const msg = errorMessage(err);
16
+ if (msg.includes("could not acquire lock")) {
17
+ return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
18
+ }
19
+ throw err;
20
+ }
21
+ }
22
+ export function resolveActiveProfile(phrenPath, requestedProfile) {
23
+ const manifest = readRootManifest(phrenPath);
24
+ if (manifest?.installMode === "project-local") {
25
+ return phrenOk(undefined);
26
+ }
27
+ if (requestedProfile) {
28
+ const profiles = listProfiles(phrenPath);
29
+ if (!profiles.ok)
30
+ return forwardErr(profiles);
31
+ const exists = profiles.data.some((entry) => entry.name === requestedProfile);
32
+ if (!exists)
33
+ return phrenErr(`Profile "${requestedProfile}" not found.`, PhrenError.NOT_FOUND);
34
+ return phrenOk(requestedProfile);
35
+ }
36
+ const machines = listMachines(phrenPath);
37
+ if (machines.ok) {
38
+ const profiles = listProfiles(phrenPath);
39
+ if (!profiles.ok)
40
+ return phrenOk(undefined);
41
+ const candidates = [getMachineName(), defaultMachineName()].filter((value, index, values) => value && values.indexOf(value) === index);
42
+ for (const machineName of candidates) {
43
+ const mapped = machines.data[machineName];
44
+ if (!mapped)
45
+ continue;
46
+ const exists = profiles.data.some((entry) => entry.name === mapped);
47
+ if (exists)
48
+ return phrenOk(mapped);
49
+ }
50
+ }
51
+ const profiles = listProfiles(phrenPath);
52
+ if (!profiles.ok)
53
+ return phrenOk(undefined);
54
+ return phrenOk(profiles.data[0]?.name);
55
+ }
56
+ export function listMachines(phrenPath) {
57
+ const machinesPath = path.join(phrenPath, "machines.yaml");
58
+ if (!fs.existsSync(machinesPath))
59
+ return phrenErr(`machines.yaml not found. Run 'npx phren init' to set up your phren.`, PhrenError.FILE_NOT_FOUND);
60
+ try {
61
+ const raw = fs.readFileSync(machinesPath, "utf8");
62
+ const parsed = yaml.load(raw, { schema: yaml.CORE_SCHEMA });
63
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
64
+ return phrenErr(`machines.yaml is empty or not valid YAML. Check the file format or run 'phren doctor --fix'.`, PhrenError.MALFORMED_YAML);
65
+ const cleaned = {};
66
+ for (const [machine, profile] of Object.entries(parsed)) {
67
+ if (typeof machine !== "string" || !machine.trim())
68
+ continue;
69
+ if (typeof profile !== "string" || !profile.trim())
70
+ continue;
71
+ cleaned[machine] = profile;
72
+ }
73
+ return phrenOk(cleaned);
74
+ }
75
+ catch (err) {
76
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
77
+ process.stderr.write(`[phren] listMachines yaml parse: ${errorMessage(err)}\n`);
78
+ return phrenErr(`Could not parse machines.yaml. Check the file for syntax errors or run 'phren doctor --fix'.`, PhrenError.MALFORMED_YAML);
79
+ }
80
+ }
81
+ function writeMachines(phrenPath, data) {
82
+ const machinesPath = path.join(phrenPath, "machines.yaml");
83
+ const backupPath = `${machinesPath}.bak`;
84
+ const existing = fs.existsSync(machinesPath) ? fs.readFileSync(machinesPath, "utf8") : "";
85
+ if (fs.existsSync(machinesPath))
86
+ fs.copyFileSync(machinesPath, backupPath);
87
+ const ordered = Object.fromEntries(Object.entries(data).sort(([a], [b]) => a.localeCompare(b)));
88
+ const headerLines = [];
89
+ for (const line of existing.split("\n")) {
90
+ if (line.startsWith("#") || line.trim() === "") {
91
+ headerLines.push(line);
92
+ continue;
93
+ }
94
+ break;
95
+ }
96
+ const header = headerLines.length ? `${headerLines.join("\n")}\n` : "";
97
+ const tmpPath = `${machinesPath}.tmp-${crypto.randomUUID()}`;
98
+ fs.writeFileSync(tmpPath, header + yaml.dump(ordered, { lineWidth: 1000 }));
99
+ fs.renameSync(tmpPath, machinesPath);
100
+ }
101
+ export function setMachineProfile(phrenPath, machine, profile) {
102
+ if (!machine || !profile)
103
+ return phrenErr(`Both machine name and profile name are required. Example: :machine map my-laptop personal`, PhrenError.EMPTY_INPUT);
104
+ const profiles = listProfiles(phrenPath);
105
+ if (profiles.ok) {
106
+ const exists = profiles.data.some((entry) => entry.name === profile);
107
+ if (!exists)
108
+ return phrenErr(`Profile "${profile}" does not exist. Check available profiles in the profiles/ directory.`, PhrenError.NOT_FOUND);
109
+ }
110
+ const machinesPath = path.join(phrenPath, "machines.yaml");
111
+ return withSafeLock(machinesPath, () => {
112
+ const current = listMachines(phrenPath);
113
+ const data = current.ok ? current.data : {};
114
+ data[machine] = profile;
115
+ writeMachines(phrenPath, data);
116
+ return phrenOk(`Mapped machine ${machine} -> ${profile}.`);
117
+ });
118
+ }
119
+ export function listProfiles(phrenPath) {
120
+ const profilesDir = path.join(phrenPath, "profiles");
121
+ if (!fs.existsSync(profilesDir))
122
+ return phrenErr(`No profiles/ directory found. Run 'npx phren init' to set up your phren.`, PhrenError.FILE_NOT_FOUND);
123
+ const files = fs.readdirSync(profilesDir).filter((file) => file.endsWith(".yaml")).sort();
124
+ const profiles = [];
125
+ for (const file of files) {
126
+ const full = path.join(profilesDir, file);
127
+ try {
128
+ const raw = fs.readFileSync(full, "utf8");
129
+ const parsed = yaml.load(raw, { schema: yaml.CORE_SCHEMA });
130
+ const data = parsed && typeof parsed === "object" && !Array.isArray(parsed)
131
+ ? parsed
132
+ : null;
133
+ const name = (typeof data?.name === "string" && data.name.trim())
134
+ ? data.name
135
+ : file.replace(/\.yaml$/, "");
136
+ const projects = Array.isArray(data?.projects)
137
+ ? data.projects.map((project) => String(project)).filter(Boolean)
138
+ : [];
139
+ profiles.push({ name, file: full, projects });
140
+ }
141
+ catch (err) {
142
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
143
+ process.stderr.write(`[phren] listProfiles yamlParse: ${errorMessage(err)}\n`);
144
+ return phrenErr(`profiles/${file}`, PhrenError.MALFORMED_YAML);
145
+ }
146
+ }
147
+ return phrenOk(profiles);
148
+ }
149
+ function writeProfile(file, name, projects) {
150
+ const backup = `${file}.bak`;
151
+ if (fs.existsSync(file))
152
+ fs.copyFileSync(file, backup);
153
+ const normalized = [...new Set(projects)].sort();
154
+ const out = yaml.dump({ name, projects: normalized }, { lineWidth: 1000 });
155
+ const tmpPath = `${file}.tmp-${crypto.randomUUID()}`;
156
+ fs.writeFileSync(tmpPath, out);
157
+ fs.renameSync(tmpPath, file);
158
+ }
159
+ export function addProjectToProfile(phrenPath, profile, project) {
160
+ if (!isValidProjectName(project))
161
+ return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
162
+ const profiles = listProfiles(phrenPath);
163
+ if (!profiles.ok)
164
+ return forwardErr(profiles);
165
+ const current = profiles.data.find((entry) => entry.name === profile);
166
+ if (!current)
167
+ return phrenErr(`Profile "${profile}" not found.`, PhrenError.NOT_FOUND);
168
+ return withSafeLock(current.file, () => {
169
+ const refreshed = listProfiles(phrenPath);
170
+ if (!refreshed.ok)
171
+ return forwardErr(refreshed);
172
+ const latest = refreshed.data.find((entry) => entry.name === profile);
173
+ if (!latest)
174
+ return phrenErr(`Profile "${profile}" not found.`, PhrenError.NOT_FOUND);
175
+ const projects = latest.projects.includes(project) ? latest.projects : [...latest.projects, project];
176
+ writeProfile(latest.file, latest.name, projects);
177
+ return phrenOk(`Added ${project} to profile ${profile}.`);
178
+ });
179
+ }
180
+ export function removeProjectFromProfile(phrenPath, profile, project) {
181
+ const profiles = listProfiles(phrenPath);
182
+ if (!profiles.ok)
183
+ return forwardErr(profiles);
184
+ const current = profiles.data.find((entry) => entry.name === profile);
185
+ if (!current)
186
+ return phrenErr(`Profile "${profile}" not found.`, PhrenError.NOT_FOUND);
187
+ return withSafeLock(current.file, () => {
188
+ const refreshed = listProfiles(phrenPath);
189
+ if (!refreshed.ok)
190
+ return forwardErr(refreshed);
191
+ const latest = refreshed.data.find((entry) => entry.name === profile);
192
+ if (!latest)
193
+ return phrenErr(`Profile "${profile}" not found.`, PhrenError.NOT_FOUND);
194
+ const projects = latest.projects.filter((entry) => entry !== project);
195
+ writeProfile(latest.file, latest.name, projects);
196
+ return phrenOk(`Removed ${project} from profile ${profile}.`);
197
+ });
198
+ }
199
+ function buildProjectCard(dir) {
200
+ const name = path.basename(dir);
201
+ const summaryFile = path.join(dir, "summary.md");
202
+ const claudeFile = path.join(dir, "CLAUDE.md");
203
+ const summarySource = fs.existsSync(summaryFile)
204
+ ? fs.readFileSync(summaryFile, "utf8")
205
+ : fs.existsSync(claudeFile)
206
+ ? fs.readFileSync(claudeFile, "utf8")
207
+ : "";
208
+ const summary = summarySource
209
+ .split("\n")
210
+ .map((line) => line.trim())
211
+ .find((line) => line && !line.startsWith("#")) || "";
212
+ const docs = ["CLAUDE.md", "FINDINGS.md", "summary.md", "MEMORY_QUEUE.md"]
213
+ .filter((file) => fs.existsSync(path.join(dir, file)));
214
+ const taskFile = TASK_FILE_ALIASES.find((file) => fs.existsSync(path.join(dir, file)));
215
+ if (taskFile)
216
+ docs.push(taskFile);
217
+ return { name, summary, docs };
218
+ }
219
+ export function listProjectCards(phrenPath, profile) {
220
+ const dirs = getProjectDirs(phrenPath, profile).sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
221
+ const cards = dirs.map(buildProjectCard);
222
+ // Prepend global as a pinned entry so it's always accessible from the shell
223
+ const globalDir = path.join(phrenPath, "global");
224
+ if (fs.existsSync(globalDir)) {
225
+ cards.unshift(buildProjectCard(globalDir));
226
+ }
227
+ return cards;
228
+ }
@@ -0,0 +1,85 @@
1
+ import * as crypto from "crypto";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as yaml from "js-yaml";
5
+ import { readInstallPreferences } from "./init-preferences.js";
6
+ import { debugLog } from "./shared.js";
7
+ import { errorMessage } from "./utils.js";
8
+ export const PROJECT_OWNERSHIP_MODES = ["phren-managed", "detached", "repo-managed"];
9
+ export const PROJECT_HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionStart", "PostToolUse"];
10
+ export function parseProjectOwnershipMode(raw) {
11
+ if (!raw)
12
+ return undefined;
13
+ const normalized = raw.trim().toLowerCase();
14
+ if (normalized === "phren" || normalized === "managed" || normalized === "phren")
15
+ return "phren-managed";
16
+ if (normalized === "repo" || normalized === "external")
17
+ return "repo-managed";
18
+ if (PROJECT_OWNERSHIP_MODES.includes(normalized)) {
19
+ return normalized;
20
+ }
21
+ return undefined;
22
+ }
23
+ export function projectConfigPath(phrenPath, project) {
24
+ return path.join(phrenPath, project, "phren.project.yaml");
25
+ }
26
+ export function readProjectConfig(phrenPath, project) {
27
+ const configPath = projectConfigPath(phrenPath, project);
28
+ if (!fs.existsSync(configPath))
29
+ return {};
30
+ try {
31
+ const parsed = yaml.load(fs.readFileSync(configPath, "utf8"), { schema: yaml.CORE_SCHEMA });
32
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
33
+ }
34
+ catch (err) {
35
+ debugLog(`readProjectConfig: failed to parse ${configPath}: ${errorMessage(err)}`);
36
+ return {};
37
+ }
38
+ }
39
+ export function writeProjectConfig(phrenPath, project, patch) {
40
+ const configPath = projectConfigPath(phrenPath, project);
41
+ const current = readProjectConfig(phrenPath, project);
42
+ const next = {
43
+ ...current,
44
+ ...patch,
45
+ };
46
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
47
+ const tmpPath = `${configPath}.tmp-${crypto.randomUUID()}`;
48
+ fs.writeFileSync(tmpPath, yaml.dump(next, { lineWidth: 1000 }));
49
+ fs.renameSync(tmpPath, configPath);
50
+ return next;
51
+ }
52
+ export function getProjectSourcePath(phrenPath, project, config) {
53
+ const raw = (config ?? readProjectConfig(phrenPath, project)).sourcePath;
54
+ return typeof raw === "string" && raw.trim() ? path.resolve(raw) : undefined;
55
+ }
56
+ export function getProjectOwnershipDefault(phrenPath) {
57
+ return parseProjectOwnershipMode(readInstallPreferences(phrenPath).projectOwnershipDefault) ?? "phren-managed";
58
+ }
59
+ export function getProjectOwnershipMode(phrenPath, project, config) {
60
+ return parseProjectOwnershipMode((config ?? readProjectConfig(phrenPath, project)).ownership) ?? "phren-managed";
61
+ }
62
+ function normalizeHookConfig(config) {
63
+ const hooks = config?.hooks;
64
+ return hooks && typeof hooks === "object" ? hooks : {};
65
+ }
66
+ export function isProjectHookEnabled(phrenPath, project, event, config) {
67
+ if (!project)
68
+ return true;
69
+ const hooks = normalizeHookConfig(config ?? readProjectConfig(phrenPath, project));
70
+ const eventValue = hooks[event];
71
+ if (typeof eventValue === "boolean")
72
+ return eventValue;
73
+ if (typeof hooks.enabled === "boolean")
74
+ return hooks.enabled;
75
+ return true;
76
+ }
77
+ export function writeProjectHookConfig(phrenPath, project, patch) {
78
+ const current = readProjectConfig(phrenPath, project);
79
+ return writeProjectConfig(phrenPath, project, {
80
+ hooks: {
81
+ ...normalizeHookConfig(current),
82
+ ...patch,
83
+ },
84
+ });
85
+ }
@@ -0,0 +1,25 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { debugLog, homeDir, homePath } from "./shared.js";
4
+ import { errorMessage } from "./utils.js";
5
+ const DEFAULT_SEARCH_PATHS = [
6
+ homeDir(),
7
+ homePath("Sites"),
8
+ homePath("Projects"),
9
+ homePath("Code"),
10
+ homePath("dev"),
11
+ ];
12
+ export function findProjectDir(name) {
13
+ const extra = process.env.PROJECTS_DIR ? [process.env.PROJECTS_DIR] : [];
14
+ for (const base of [...extra, ...DEFAULT_SEARCH_PATHS]) {
15
+ const candidate = path.join(base, name);
16
+ try {
17
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory())
18
+ return candidate;
19
+ }
20
+ catch (err) {
21
+ debugLog(`findProjectDir: failed to check ${candidate}: ${errorMessage(err)}`);
22
+ }
23
+ }
24
+ return null;
25
+ }