@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,108 @@
1
+ // Shared Phren result types, validation tags, and low-level helpers.
2
+ /**
3
+ * Minimal cross-domain starter set for entity/conflict detection.
4
+ *
5
+ * Kept intentionally small: only terms that are genuinely universal across
6
+ * disciplines (languages, infra primitives, version control). Framework-specific
7
+ * tools (React, Django, Unity, JUCE, Ansible, ...) are learned dynamically from
8
+ * each project's FINDINGS.md via extractDynamicEntities().
9
+ */
10
+ export const UNIVERSAL_TECH_TERMS_RE = /\b(Python|Rust|Go|Java|TypeScript|JavaScript|Docker|Kubernetes|AWS|GCP|Azure|SQL|Git)\b/gi;
11
+ /**
12
+ * Additional entity patterns beyond CamelCase and acronyms.
13
+ * Each pattern has a named group so callers can identify the entity type.
14
+ */
15
+ export const EXTRA_ENTITY_PATTERNS = [
16
+ // Semantic version numbers: v1.2.3, 2.0.0-beta.1
17
+ { re: /\bv?\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?\b/g, label: "version" },
18
+ // Environment variable keys: PHREN_*, NODE_ENV, etc. (2+ uppercase segments separated by _)
19
+ { re: /\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, label: "env_key" },
20
+ // File paths: at least one slash with an extension or known dir prefix
21
+ { re: /(?:~\/|\.\/|\/)[a-zA-Z0-9_\-./]+\.[a-zA-Z0-9]+/g, label: "file_path" },
22
+ // Error codes: E0001, ERR_MODULE_NOT_FOUND, TS2345, etc.
23
+ { re: /\b(?:ERR_[A-Z0-9_]{3,}|(?:TS|RS|PY|E)\d{3,})\b/g, label: "error_code" },
24
+ // ISO date references: 2025-03-11, 2025/03/11
25
+ { re: /\b\d{4}[-/]\d{2}[-/]\d{2}\b/g, label: "date" },
26
+ ];
27
+ // Default timeout for execFileSync calls (30s for most operations, 10s for quick probes like `which`)
28
+ export const EXEC_TIMEOUT_MS = 30_000;
29
+ export const EXEC_TIMEOUT_QUICK_MS = 10_000;
30
+ // Structured error codes for consistent error handling across data-access and MCP tools
31
+ export const PhrenError = {
32
+ PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
33
+ INVALID_PROJECT_NAME: "INVALID_PROJECT_NAME",
34
+ FILE_NOT_FOUND: "FILE_NOT_FOUND",
35
+ PERMISSION_DENIED: "PERMISSION_DENIED",
36
+ MALFORMED_JSON: "MALFORMED_JSON",
37
+ MALFORMED_YAML: "MALFORMED_YAML",
38
+ NOT_FOUND: "NOT_FOUND",
39
+ AMBIGUOUS_MATCH: "AMBIGUOUS_MATCH",
40
+ LOCK_TIMEOUT: "LOCK_TIMEOUT",
41
+ EMPTY_INPUT: "EMPTY_INPUT",
42
+ VALIDATION_ERROR: "VALIDATION_ERROR",
43
+ INDEX_ERROR: "INDEX_ERROR",
44
+ NETWORK_ERROR: "NETWORK_ERROR",
45
+ };
46
+ export function phrenOk(data) {
47
+ return { ok: true, data };
48
+ }
49
+ export function phrenErr(error, code) {
50
+ return { ok: false, error, code };
51
+ }
52
+ // Forward a failed PhrenResult to a different result type (re-types the error branch).
53
+ // Safe to call after an `if (!result.ok)` guard; extracts error and code from the union.
54
+ export function forwardErr(result) {
55
+ if (!result.ok)
56
+ return { ok: false, error: result.error, code: result.code };
57
+ return { ok: false, error: "unexpected forward of ok result" };
58
+ }
59
+ const ERROR_CODES = new Set(Object.values(PhrenError));
60
+ // Extract the error code from an error string (e.g. "PROJECT_NOT_FOUND: ...").
61
+ // Returns the code if the string starts with a known PhrenError, or undefined.
62
+ export function parsePhrenErrorCode(msg) {
63
+ const prefix = msg.split(":")[0]?.trim();
64
+ if (prefix && ERROR_CODES.has(prefix))
65
+ return prefix;
66
+ return undefined;
67
+ }
68
+ export function isRecord(value) {
69
+ return typeof value === "object" && value !== null && !Array.isArray(value);
70
+ }
71
+ /** Shallow-merge data onto defaults so missing keys get filled in. */
72
+ export function withDefaults(data, defaults) {
73
+ const merged = { ...defaults };
74
+ for (const key of Object.keys(data)) {
75
+ const val = data[key];
76
+ if (val !== undefined && val !== null) {
77
+ if (typeof val === "object" && !Array.isArray(val) && typeof merged[key] === "object" && !Array.isArray(merged[key])) {
78
+ merged[key] = { ...merged[key], ...val };
79
+ }
80
+ else {
81
+ merged[key] = val;
82
+ }
83
+ }
84
+ }
85
+ return merged;
86
+ }
87
+ /** All valid finding type tags — used for writes, search filters, and hook extraction */
88
+ export const FINDING_TYPES = ["decision", "pitfall", "pattern", "tradeoff", "architecture", "bug"];
89
+ /** Searchable finding tags (same set as FINDING_TYPES) */
90
+ export const FINDING_TAGS = FINDING_TYPES;
91
+ /** Canonical set of known observation tags — derived from FINDING_TYPES */
92
+ export const KNOWN_OBSERVATION_TAGS = new Set(FINDING_TYPES);
93
+ /** Document types in the FTS index */
94
+ export const DOC_TYPES = ["claude", "findings", "reference", "skills", "summary", "task", "changelog", "canonical", "memory-queue", "skill", "other"];
95
+ // ── Cache eviction helper ────────────────────────────────────────────────────
96
+ const CACHE_MAX = 1000;
97
+ const CACHE_EVICT = 100;
98
+ export function capCache(cache) {
99
+ if (cache.size > CACHE_MAX) {
100
+ const it = cache.keys();
101
+ for (let i = 0; i < CACHE_EVICT; i++) {
102
+ const k = it.next();
103
+ if (k.done)
104
+ break;
105
+ cache.delete(k.value);
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,67 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ let _loadedEnvKey;
5
+ let _loadedEnvPath = null;
6
+ let _loadedEnvMtimeMs = -1;
7
+ function homeDir() {
8
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
9
+ }
10
+ function parseAndApplyDotEnv(filePath) {
11
+ const content = fs.readFileSync(filePath, "utf8");
12
+ for (const line of content.split("\n")) {
13
+ const trimmed = line.trim();
14
+ if (!trimmed || trimmed.startsWith("#"))
15
+ continue;
16
+ const eqIdx = trimmed.indexOf("=");
17
+ if (eqIdx < 1)
18
+ continue;
19
+ const key = trimmed.slice(0, eqIdx).trim();
20
+ const raw = trimmed.slice(eqIdx + 1).trim();
21
+ const value = /^(["'])(.*)\1$/.test(raw) ? raw.slice(1, -1) : raw;
22
+ if (key && !(key in process.env))
23
+ process.env[key] = value;
24
+ }
25
+ }
26
+ function resolveDotEnvPath(phrenPath) {
27
+ const envPath = process.env.PHREN_PATH;
28
+ const candidates = [
29
+ phrenPath ? path.join(phrenPath, ".env") : null,
30
+ envPath ? path.join(envPath, ".env") : null,
31
+ path.join(homeDir(), ".phren", ".env"),
32
+ ].filter((candidate) => Boolean(candidate));
33
+ const seen = new Set();
34
+ for (const candidate of candidates) {
35
+ const resolved = path.resolve(candidate);
36
+ if (seen.has(resolved))
37
+ continue;
38
+ seen.add(resolved);
39
+ if (fs.existsSync(resolved))
40
+ return resolved;
41
+ }
42
+ return null;
43
+ }
44
+ export function bootstrapPhrenDotEnv(phrenPath) {
45
+ const cacheKey = `${phrenPath ?? ""}|${process.env.PHREN_PATH ?? ""}|${process.env.HOME ?? ""}|${process.env.USERPROFILE ?? ""}`;
46
+ const envPath = resolveDotEnvPath(phrenPath);
47
+ if (!envPath) {
48
+ _loadedEnvKey = cacheKey;
49
+ _loadedEnvPath = null;
50
+ _loadedEnvMtimeMs = -1;
51
+ return null;
52
+ }
53
+ const mtimeMs = fs.statSync(envPath).mtimeMs;
54
+ if (_loadedEnvKey === cacheKey && _loadedEnvPath === envPath && _loadedEnvMtimeMs === mtimeMs) {
55
+ return envPath;
56
+ }
57
+ parseAndApplyDotEnv(envPath);
58
+ _loadedEnvKey = cacheKey;
59
+ _loadedEnvPath = envPath;
60
+ _loadedEnvMtimeMs = mtimeMs;
61
+ return envPath;
62
+ }
63
+ export function resetPhrenDotEnvBootstrapForTests() {
64
+ _loadedEnvKey = undefined;
65
+ _loadedEnvPath = null;
66
+ _loadedEnvMtimeMs = -1;
67
+ }
@@ -0,0 +1,476 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import * as crypto from "crypto";
5
+ import * as yaml from "js-yaml";
6
+ import { bootstrapPhrenDotEnv } from "./phren-dotenv.js";
7
+ import { PhrenError, isRecord } from "./phren-core.js";
8
+ import { errorMessage, isValidProjectName, safeProjectPath } from "./utils.js";
9
+ bootstrapPhrenDotEnv();
10
+ export const ROOT_MANIFEST_FILENAME = "phren.root.yaml";
11
+ export function homeDir() {
12
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
13
+ }
14
+ export function homePath(...parts) {
15
+ return path.join(homeDir(), ...parts);
16
+ }
17
+ export function expandHomePath(input) {
18
+ if (input === "~")
19
+ return homeDir();
20
+ if (input.startsWith("~/") || input.startsWith("~\\"))
21
+ return path.join(homeDir(), input.slice(2));
22
+ return input;
23
+ }
24
+ export function defaultPhrenPath() {
25
+ return expandHomePath(process.env.PHREN_PATH || homePath(".phren"));
26
+ }
27
+ export function rootManifestPath(phrenPath) {
28
+ return path.join(phrenPath, ROOT_MANIFEST_FILENAME);
29
+ }
30
+ export function atomicWriteText(filePath, content) {
31
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
32
+ const tmpPath = `${filePath}.tmp-${crypto.randomUUID()}`;
33
+ fs.writeFileSync(tmpPath, content);
34
+ fs.renameSync(tmpPath, filePath);
35
+ }
36
+ function isInstallMode(value) {
37
+ return value === "shared" || value === "project-local";
38
+ }
39
+ function isSyncMode(value) {
40
+ return value === "managed-git" || value === "workspace-git";
41
+ }
42
+ function normalizeManifest(raw) {
43
+ if (!isRecord(raw))
44
+ return null;
45
+ const version = Number(raw.version);
46
+ const installMode = raw.installMode;
47
+ const syncMode = raw.syncMode;
48
+ if (version !== 1 || !isInstallMode(installMode) || !isSyncMode(syncMode))
49
+ return null;
50
+ const workspaceRoot = typeof raw.workspaceRoot === "string" && raw.workspaceRoot.trim()
51
+ ? path.resolve(expandHomePath(raw.workspaceRoot))
52
+ : undefined;
53
+ const primaryProject = typeof raw.primaryProject === "string" && raw.primaryProject.trim()
54
+ ? raw.primaryProject.trim()
55
+ : undefined;
56
+ if (installMode === "project-local") {
57
+ if (!workspaceRoot || !primaryProject || !isValidProjectName(primaryProject))
58
+ return null;
59
+ }
60
+ return {
61
+ version: 1,
62
+ installMode,
63
+ syncMode,
64
+ workspaceRoot,
65
+ primaryProject,
66
+ };
67
+ }
68
+ export function readRootManifest(phrenPath) {
69
+ const manifestFile = rootManifestPath(phrenPath);
70
+ if (!fs.existsSync(manifestFile))
71
+ return null;
72
+ try {
73
+ const parsed = yaml.load(fs.readFileSync(manifestFile, "utf8"), { schema: yaml.CORE_SCHEMA });
74
+ return normalizeManifest(parsed);
75
+ }
76
+ catch (err) {
77
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
78
+ process.stderr.write(`[phren] readRootManifest: ${errorMessage(err)}\n`);
79
+ return null;
80
+ }
81
+ }
82
+ export function writeRootManifest(phrenPath, manifest) {
83
+ const normalized = normalizeManifest(manifest);
84
+ if (!normalized) {
85
+ throw new Error(`${PhrenError.VALIDATION_ERROR}: invalid phren root manifest for ${phrenPath}`);
86
+ }
87
+ atomicWriteText(rootManifestPath(phrenPath), yaml.dump(normalized, { lineWidth: 1000 }));
88
+ }
89
+ export function resolveInstallContext(phrenPath) {
90
+ const resolvedPath = path.resolve(phrenPath);
91
+ const manifest = readRootManifest(resolvedPath);
92
+ if (!manifest) {
93
+ throw new Error(`${PhrenError.NOT_FOUND}: phren root manifest not found: ${rootManifestPath(resolvedPath)}`);
94
+ }
95
+ return { phrenPath: resolvedPath, ...manifest };
96
+ }
97
+ function requireDirectory(resolved, label) {
98
+ if (!fs.existsSync(resolved)) {
99
+ throw new Error(`${PhrenError.NOT_FOUND}: ${label} not found: ${resolved}`);
100
+ }
101
+ if (!fs.statSync(resolved).isDirectory()) {
102
+ throw new Error(`${PhrenError.VALIDATION_ERROR}: ${label} is not a directory: ${resolved}`);
103
+ }
104
+ return resolved;
105
+ }
106
+ function hasRootManifest(candidate) {
107
+ return fs.existsSync(rootManifestPath(candidate));
108
+ }
109
+ function hasInstallMarkers(candidate) {
110
+ return fs.existsSync(path.join(candidate, "machines.yaml"))
111
+ || fs.existsSync(path.join(candidate, ".governance"))
112
+ || fs.existsSync(path.join(candidate, "global"));
113
+ }
114
+ function isPhrenRootCandidate(candidate) {
115
+ return hasRootManifest(candidate) || hasInstallMarkers(candidate);
116
+ }
117
+ export function findNearestPhrenPath(startDir = process.cwd()) {
118
+ let current = path.resolve(startDir);
119
+ while (true) {
120
+ const localCandidate = path.join(current, ".phren");
121
+ if (isPhrenRootCandidate(localCandidate))
122
+ return localCandidate;
123
+ const parent = path.dirname(current);
124
+ if (parent === current)
125
+ break;
126
+ current = parent;
127
+ }
128
+ return null;
129
+ }
130
+ function sharedRootCandidate() {
131
+ return homePath(".phren");
132
+ }
133
+ let cachedPhrenPath;
134
+ let cachedPhrenPathKey;
135
+ export function findPhrenPath() {
136
+ const cacheKey = [
137
+ ((process.env.PHREN_PATH || process.env.PHREN_PATH) ?? ""),
138
+ process.env.HOME ?? "",
139
+ process.env.USERPROFILE ?? "",
140
+ process.cwd(),
141
+ ].join("|");
142
+ if (cachedPhrenPath !== undefined && cachedPhrenPathKey === cacheKey)
143
+ return cachedPhrenPath;
144
+ cachedPhrenPathKey = cacheKey;
145
+ const envVal = (process.env.PHREN_PATH || process.env.PHREN_PATH)?.trim();
146
+ if (envVal) {
147
+ const resolved = path.resolve(expandHomePath(envVal));
148
+ cachedPhrenPath = isPhrenRootCandidate(resolved) ? resolved : null;
149
+ return cachedPhrenPath;
150
+ }
151
+ const nearest = findNearestPhrenPath();
152
+ if (nearest) {
153
+ cachedPhrenPath = nearest;
154
+ return nearest;
155
+ }
156
+ const shared = sharedRootCandidate();
157
+ cachedPhrenPath = isPhrenRootCandidate(shared) ? shared : null;
158
+ return cachedPhrenPath;
159
+ }
160
+ export function ensurePhrenPath() {
161
+ const existing = findPhrenPath();
162
+ if (existing)
163
+ return existing;
164
+ const defaultPath = sharedRootCandidate();
165
+ fs.mkdirSync(defaultPath, { recursive: true });
166
+ writeRootManifest(defaultPath, {
167
+ version: 1,
168
+ installMode: "shared",
169
+ syncMode: "managed-git",
170
+ });
171
+ cachedPhrenPath = defaultPath;
172
+ cachedPhrenPathKey = [
173
+ ((process.env.PHREN_PATH || process.env.PHREN_PATH) ?? ""),
174
+ process.env.HOME ?? "",
175
+ process.env.USERPROFILE ?? "",
176
+ process.cwd(),
177
+ ].join("|");
178
+ return defaultPath;
179
+ }
180
+ export function findPhrenPathWithArg(arg) {
181
+ if (arg) {
182
+ const resolved = requireDirectory(path.resolve(expandHomePath(arg)), "phren path");
183
+ if (!hasRootManifest(resolved)) {
184
+ throw new Error(`${PhrenError.NOT_FOUND}: phren root manifest not found: ${rootManifestPath(resolved)}`);
185
+ }
186
+ return resolved;
187
+ }
188
+ const existing = findPhrenPath();
189
+ if (existing)
190
+ return existing;
191
+ throw new Error(`${PhrenError.NOT_FOUND}: phren root not found. Run 'npx phren init'.`);
192
+ }
193
+ export function isProjectLocalMode(phrenPath) {
194
+ try {
195
+ return resolveInstallContext(phrenPath).installMode === "project-local";
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ }
201
+ // Centralized runtime path helpers. All ephemeral/runtime files go in
202
+ // subdirectories to keep the phren root clean.
203
+ export function runtimeDir(phrenPath) {
204
+ return path.join(phrenPath, ".runtime");
205
+ }
206
+ /** Unlink a file, ignoring ENOENT. Rethrows any other error. */
207
+ export function tryUnlink(filePath) {
208
+ try {
209
+ fs.unlinkSync(filePath);
210
+ }
211
+ catch (e) {
212
+ if (e.code !== "ENOENT")
213
+ throw e;
214
+ }
215
+ }
216
+ export function sessionsDir(phrenPath) {
217
+ return path.join(phrenPath, ".sessions");
218
+ }
219
+ const runtimeDirsMade = new Set();
220
+ export function runtimeFile(phrenPath, name) {
221
+ const dir = runtimeDir(phrenPath);
222
+ if (!runtimeDirsMade.has(dir)) {
223
+ fs.mkdirSync(dir, { recursive: true });
224
+ runtimeDirsMade.add(dir);
225
+ }
226
+ return path.join(dir, name);
227
+ }
228
+ export function installPreferencesFile(phrenPath) {
229
+ return path.join(runtimeDir(phrenPath), "install-preferences.json");
230
+ }
231
+ export function runtimeHealthFile(phrenPath) {
232
+ return path.join(runtimeDir(phrenPath), "runtime-health.json");
233
+ }
234
+ export function shellStateFile(phrenPath) {
235
+ return path.join(runtimeDir(phrenPath), "shell-state.json");
236
+ }
237
+ export function sessionMetricsFile(phrenPath) {
238
+ return path.join(runtimeDir(phrenPath), "session-metrics.json");
239
+ }
240
+ export function memoryScoresFile(phrenPath) {
241
+ return path.join(runtimeDir(phrenPath), "memory-scores.json");
242
+ }
243
+ export function memoryUsageLogFile(phrenPath) {
244
+ return path.join(runtimeDir(phrenPath), "memory-usage.log");
245
+ }
246
+ export function sessionMarker(phrenPath, name) {
247
+ const dir = sessionsDir(phrenPath);
248
+ fs.mkdirSync(dir, { recursive: true });
249
+ return path.join(dir, name);
250
+ }
251
+ // Debug logging is best-effort and only writes when a phren root already exists.
252
+ export function debugLog(msg) {
253
+ if (!(process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
254
+ return;
255
+ const phrenPath = findPhrenPath();
256
+ if (!phrenPath)
257
+ return;
258
+ const logFile = runtimeFile(phrenPath, "debug.log");
259
+ try {
260
+ fs.appendFileSync(logFile, `[${new Date().toISOString()}] ${msg}\n`);
261
+ }
262
+ catch {
263
+ // debug log is best-effort; logging errors about logging would recurse
264
+ }
265
+ }
266
+ export function appendIndexEvent(phrenPath, event) {
267
+ try {
268
+ const file = runtimeFile(phrenPath, "index-events.jsonl");
269
+ fs.appendFileSync(file, JSON.stringify({ at: new Date().toISOString(), ...event }) + "\n");
270
+ }
271
+ catch (err) {
272
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
273
+ process.stderr.write(`[phren] appendIndexEvent: ${errorMessage(err)}\n`);
274
+ }
275
+ }
276
+ /** Resolve the canonical findings file for a project directory. */
277
+ export function resolveFindingsPath(projectDir) {
278
+ const findingsPath = path.join(projectDir, "FINDINGS.md");
279
+ if (fs.existsSync(findingsPath))
280
+ return findingsPath;
281
+ return undefined;
282
+ }
283
+ const RESERVED_PROJECT_DIR_NAMES = new Set(["profiles", "templates", "global"]);
284
+ function isProjectDirEntry(entry) {
285
+ return entry.isDirectory()
286
+ && !entry.name.startsWith(".")
287
+ && !entry.name.endsWith(".archived")
288
+ && !RESERVED_PROJECT_DIR_NAMES.has(entry.name);
289
+ }
290
+ export function normalizeProjectNameForCreate(name) {
291
+ return name.trim().toLowerCase();
292
+ }
293
+ export function findProjectNameCaseInsensitive(phrenPath, name) {
294
+ const needle = name.toLowerCase();
295
+ try {
296
+ for (const entry of fs.readdirSync(phrenPath, { withFileTypes: true })) {
297
+ if (!isProjectDirEntry(entry))
298
+ continue;
299
+ if (entry.name.toLowerCase() === needle)
300
+ return entry.name;
301
+ }
302
+ }
303
+ catch (err) {
304
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
305
+ process.stderr.write(`[phren] findProjectNameCaseInsensitive: ${errorMessage(err)}\n`);
306
+ }
307
+ return null;
308
+ }
309
+ function getLocalProjectDirs(phrenPath, manifest) {
310
+ const primaryProject = manifest.primaryProject;
311
+ if (!primaryProject || !isValidProjectName(primaryProject))
312
+ return [];
313
+ const projectPath = safeProjectPath(phrenPath, primaryProject);
314
+ if (!projectPath || !fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory())
315
+ return [];
316
+ const visible = fs.readdirSync(phrenPath, { withFileTypes: true }).filter(isProjectDirEntry).map((entry) => entry.name);
317
+ if (visible.length !== 1 || visible[0] !== primaryProject)
318
+ return [];
319
+ return [projectPath];
320
+ }
321
+ // Figure out which project directories to index.
322
+ export function getProjectDirs(phrenPath, profile) {
323
+ const manifest = readRootManifest(phrenPath);
324
+ if (manifest?.installMode === "project-local") {
325
+ return getLocalProjectDirs(phrenPath, manifest);
326
+ }
327
+ if (profile) {
328
+ if (!isValidProjectName(profile)) {
329
+ console.error(`${PhrenError.VALIDATION_ERROR}: Invalid PHREN_PROFILE value: ${profile}`);
330
+ return [];
331
+ }
332
+ const profilePath = path.join(phrenPath, "profiles", `${profile}.yaml`);
333
+ if (!fs.existsSync(profilePath)) {
334
+ console.error(`${PhrenError.FILE_NOT_FOUND}: Profile file not found: ${profilePath}`);
335
+ return [];
336
+ }
337
+ try {
338
+ const data = yaml.load(fs.readFileSync(profilePath, "utf-8"), { schema: yaml.CORE_SCHEMA });
339
+ const projects = isRecord(data) ? data.projects : undefined;
340
+ if (!Array.isArray(projects)) {
341
+ console.error(`${PhrenError.MALFORMED_YAML}: Profile YAML missing valid "projects" array: ${profilePath}`);
342
+ return [];
343
+ }
344
+ const listed = projects
345
+ .map((p) => {
346
+ const name = String(p);
347
+ if (!isValidProjectName(name)) {
348
+ console.error(`${PhrenError.VALIDATION_ERROR}: Skipping invalid project name in profile: ${name}`);
349
+ return null;
350
+ }
351
+ return safeProjectPath(phrenPath, name);
352
+ })
353
+ .filter((p) => p !== null && fs.existsSync(p));
354
+ const sharedDirs = ["shared", "org"]
355
+ .map((name) => safeProjectPath(phrenPath, name))
356
+ .filter((p) => Boolean(p && fs.existsSync(p) && fs.statSync(p).isDirectory()));
357
+ return [...new Set([...listed, ...sharedDirs])];
358
+ }
359
+ catch (err) {
360
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
361
+ process.stderr.write(`[phren] getProjectDirs yamlParse: ${errorMessage(err)}\n`);
362
+ console.error(`${PhrenError.MALFORMED_YAML}: Malformed profile YAML: ${profilePath}`);
363
+ return [];
364
+ }
365
+ }
366
+ try {
367
+ return fs.readdirSync(phrenPath, { withFileTypes: true })
368
+ .filter(isProjectDirEntry)
369
+ .map((entry) => path.join(phrenPath, entry.name));
370
+ }
371
+ catch (err) {
372
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
373
+ process.stderr.write(`[phren] getProjectDirs: ${errorMessage(err)}\n`);
374
+ return [];
375
+ }
376
+ }
377
+ // Collect MEMORY*.md files from native agent memory locations (~/.claude/projects/*/memory/)
378
+ export function collectNativeMemoryFiles() {
379
+ const claudeProjectsDir = homePath(".claude", "projects");
380
+ if (!fs.existsSync(claudeProjectsDir))
381
+ return [];
382
+ const results = [];
383
+ try {
384
+ for (const entry of fs.readdirSync(claudeProjectsDir)) {
385
+ const memDir = path.join(claudeProjectsDir, entry, "memory");
386
+ if (!fs.existsSync(memDir))
387
+ continue;
388
+ for (const file of fs.readdirSync(memDir)) {
389
+ if (!file.endsWith(".md") || file === "MEMORY.md")
390
+ continue;
391
+ const fullPath = path.join(memDir, file);
392
+ const match = file.match(/^MEMORY-(.+)\.md$/);
393
+ const project = match ? match[1] : `native:${entry}`;
394
+ results.push({ project, file, fullPath });
395
+ }
396
+ }
397
+ }
398
+ catch (err) {
399
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
400
+ process.stderr.write(`[phren] collectNativeMemoryFiles: ${errorMessage(err)}\n`);
401
+ }
402
+ return results;
403
+ }
404
+ function pushFileToken(parts, filePath) {
405
+ try {
406
+ const stat = fs.statSync(filePath);
407
+ if (stat.isFile())
408
+ parts.push(`${filePath}:${stat.mtimeMs}:${stat.size}`);
409
+ }
410
+ catch {
411
+ parts.push(`${filePath}:missing`);
412
+ }
413
+ }
414
+ function pushDirTokens(parts, dirPath) {
415
+ if (!fs.existsSync(dirPath)) {
416
+ parts.push(`${dirPath}:missing`);
417
+ return;
418
+ }
419
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
420
+ const fullPath = path.join(dirPath, entry.name);
421
+ if (entry.isDirectory()) {
422
+ pushDirTokens(parts, fullPath);
423
+ continue;
424
+ }
425
+ const stat = fs.statSync(fullPath);
426
+ parts.push(`${fullPath}:${stat.mtimeMs}:${stat.size}`);
427
+ }
428
+ }
429
+ export function computePhrenLiveStateToken(phrenPath) {
430
+ const parts = [];
431
+ const projectDirs = getProjectDirs(phrenPath).sort();
432
+ const manifest = readRootManifest(phrenPath);
433
+ for (const projectDir of projectDirs) {
434
+ const project = path.basename(projectDir);
435
+ parts.push(`project:${project}`);
436
+ for (const file of ["CLAUDE.md", "summary.md", "FINDINGS.md", "tasks.md", "MEMORY_QUEUE.md", "CANONICAL_MEMORIES.md", "topic-config.json", "phren.project.yaml"]) {
437
+ pushFileToken(parts, path.join(projectDir, file));
438
+ }
439
+ pushDirTokens(parts, path.join(projectDir, "reference"));
440
+ pushDirTokens(parts, path.join(projectDir, "skills"));
441
+ pushDirTokens(parts, path.join(projectDir, ".claude", "skills"));
442
+ }
443
+ if (manifest?.installMode === "shared") {
444
+ pushDirTokens(parts, path.join(phrenPath, "profiles"));
445
+ }
446
+ pushDirTokens(parts, path.join(phrenPath, "global", "skills"));
447
+ pushFileToken(parts, path.join(phrenPath, ".governance", "access-control.json"));
448
+ pushFileToken(parts, rootManifestPath(phrenPath));
449
+ pushFileToken(parts, runtimeHealthFile(phrenPath));
450
+ pushFileToken(parts, runtimeFile(phrenPath, "audit.log"));
451
+ pushFileToken(parts, memoryUsageLogFile(phrenPath));
452
+ pushFileToken(parts, installPreferencesFile(phrenPath));
453
+ if (manifest?.installMode === "shared") {
454
+ pushDirTokens(parts, homePath(".github", "hooks"));
455
+ pushFileToken(parts, homePath(".cursor", "hooks.json"));
456
+ }
457
+ return parts.sort().join("|");
458
+ }
459
+ // Lazy singleton for getPhrenPath — shared across all CLI modules.
460
+ let lazyPhrenPath;
461
+ export function getPhrenPath() {
462
+ if (!lazyPhrenPath) {
463
+ const existing = findPhrenPath();
464
+ if (!existing)
465
+ throw new Error(`${PhrenError.NOT_FOUND}: phren root not found. Run 'npx phren init'.`);
466
+ lazyPhrenPath = existing;
467
+ }
468
+ return lazyPhrenPath;
469
+ }
470
+ export function qualityMarkers(phrenPathLocal) {
471
+ const today = new Date().toISOString().slice(0, 10);
472
+ return {
473
+ done: runtimeFile(phrenPathLocal, `quality-${today}`),
474
+ lock: runtimeFile(phrenPathLocal, `quality-${today}.lock`),
475
+ };
476
+ }