@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,176 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ export const HOOK_TOOL_NAMES = ["claude", "copilot", "cursor", "codex"];
5
+ function homeDir(env = process.env) {
6
+ return env.HOME || env.USERPROFILE || os.homedir();
7
+ }
8
+ function joinPortable(base, ...parts) {
9
+ const usePosix = base.startsWith("/") && !base.includes("\\");
10
+ if (usePosix) {
11
+ const normalizedBase = base.replace(/\/+$/g, "");
12
+ const normalizedParts = parts.map((part) => part.replace(/^\/+|\/+$/g, ""));
13
+ return [normalizedBase, ...normalizedParts].join("/").replace(/\/{2,}/g, "/");
14
+ }
15
+ return path.join(base, ...parts);
16
+ }
17
+ function homePathForEnv(env, ...parts) {
18
+ return joinPortable(homeDir(env), ...parts);
19
+ }
20
+ function defaultPhrenPath(env = process.env) {
21
+ return env.PHREN_PATH || env.PHREN_PATH || homePathForEnv(env, ".phren");
22
+ }
23
+ function normalizeWindowsPathToWsl(input) {
24
+ if (!input)
25
+ return undefined;
26
+ if (input.startsWith("/"))
27
+ return input;
28
+ const match = input.match(/^([A-Za-z]):\\(.*)$/);
29
+ if (!match)
30
+ return input;
31
+ const drive = match[1].toLowerCase();
32
+ const rest = match[2].replace(/\\/g, "/");
33
+ return `/mnt/${drive}/${rest}`;
34
+ }
35
+ function uniqStrings(values) {
36
+ return Array.from(new Set(values.filter((value) => Boolean(value && value.trim()))));
37
+ }
38
+ export function pickExistingFile(candidates) {
39
+ for (const candidate of candidates) {
40
+ if (fs.existsSync(candidate))
41
+ return candidate;
42
+ }
43
+ return null;
44
+ }
45
+ export function hookConfigPath(tool, phrenPath = defaultPhrenPath()) {
46
+ switch (tool) {
47
+ case "claude":
48
+ return homePathForEnv(process.env, ".claude", "settings.json");
49
+ case "copilot":
50
+ return homePathForEnv(process.env, ".github", "hooks", "phren.json");
51
+ case "cursor":
52
+ return homePathForEnv(process.env, ".cursor", "hooks.json");
53
+ case "codex":
54
+ return path.join(phrenPath || defaultPhrenPath(), "codex.json");
55
+ }
56
+ }
57
+ export function hookConfigPaths(phrenPath) {
58
+ return {
59
+ claude: hookConfigPath("claude", phrenPath),
60
+ copilot: hookConfigPath("copilot", phrenPath),
61
+ cursor: hookConfigPath("cursor", phrenPath),
62
+ codex: hookConfigPath("codex", phrenPath),
63
+ };
64
+ }
65
+ export function hookConfigRoots(phrenPath) {
66
+ const roots = new Set([path.resolve(phrenPath)]);
67
+ for (const target of Object.values(hookConfigPaths(phrenPath))) {
68
+ roots.add(path.resolve(path.dirname(target)));
69
+ }
70
+ return Array.from(roots);
71
+ }
72
+ export function vscodeMcpCandidates(env = process.env) {
73
+ const home = homeDir(env);
74
+ const userProfile = normalizeWindowsPathToWsl(env.USERPROFILE);
75
+ const username = env.USERNAME;
76
+ const userProfileRoaming = userProfile ? joinPortable(userProfile, "AppData", "Roaming", "Code", "User") : undefined;
77
+ const guessedWindowsRoaming = !userProfile && username
78
+ ? path.posix.join("/mnt/c", "Users", username, "AppData", "Roaming", "Code", "User")
79
+ : undefined;
80
+ return uniqStrings([
81
+ userProfileRoaming,
82
+ guessedWindowsRoaming,
83
+ joinPortable(home, ".config", "Code", "User"),
84
+ joinPortable(home, ".vscode-server", "data", "User"),
85
+ joinPortable(home, "Library", "Application Support", "Code", "User"),
86
+ joinPortable(home, "AppData", "Roaming", "Code", "User"),
87
+ ]);
88
+ }
89
+ export function probeVsCodeConfig(commandExists, env = process.env) {
90
+ const home = homeDir(env);
91
+ const userProfile = normalizeWindowsPathToWsl(env.USERPROFILE);
92
+ const userProfileRoaming = userProfile
93
+ ? joinPortable(userProfile, "AppData", "Roaming", "Code", "User")
94
+ : undefined;
95
+ const candidates = vscodeMcpCandidates(env);
96
+ const existing = candidates.find((candidate) => fs.existsSync(candidate));
97
+ const installed = Boolean(existing) ||
98
+ commandExists("code") ||
99
+ Boolean(userProfile &&
100
+ (fs.existsSync(joinPortable(userProfile, "AppData", "Local", "Programs", "Microsoft VS Code")) ||
101
+ fs.existsSync(joinPortable(userProfile, "AppData", "Roaming", "Code"))));
102
+ return {
103
+ targetDir: installed ? (existing || userProfileRoaming || joinPortable(home, ".config", "Code", "User")) : null,
104
+ installed,
105
+ };
106
+ }
107
+ export function cursorMcpCandidates(env = process.env) {
108
+ const home = homeDir(env);
109
+ return [
110
+ joinPortable(home, ".cursor", "mcp.json"),
111
+ joinPortable(home, ".config", "Cursor", "User", "mcp.json"),
112
+ joinPortable(home, "Library", "Application Support", "Cursor", "User", "mcp.json"),
113
+ joinPortable(home, "AppData", "Roaming", "Cursor", "User", "mcp.json"),
114
+ ];
115
+ }
116
+ export function resolveCursorMcpConfig(commandExists, env = process.env) {
117
+ const home = homeDir(env);
118
+ const candidates = cursorMcpCandidates(env);
119
+ const existing = pickExistingFile(candidates);
120
+ const installed = Boolean(existing) ||
121
+ fs.existsSync(path.join(home, ".cursor")) ||
122
+ fs.existsSync(path.join(home, ".config", "Cursor")) ||
123
+ fs.existsSync(path.join(home, "Library", "Application Support", "Cursor")) ||
124
+ fs.existsSync(path.join(home, "AppData", "Roaming", "Cursor")) ||
125
+ commandExists("cursor");
126
+ return { installed, existing, target: existing || candidates[0] };
127
+ }
128
+ export function copilotMcpCandidates(env = process.env) {
129
+ const home = homeDir(env);
130
+ return [
131
+ joinPortable(home, ".copilot", "mcp-config.json"),
132
+ joinPortable(home, ".github", "mcp.json"),
133
+ joinPortable(home, ".config", "github-copilot", "mcp.json"),
134
+ joinPortable(home, "Library", "Application Support", "github-copilot", "mcp.json"),
135
+ joinPortable(home, "AppData", "Roaming", "github-copilot", "mcp.json"),
136
+ ];
137
+ }
138
+ export function resolveCopilotMcpConfig(commandExists, env = process.env) {
139
+ const home = homeDir(env);
140
+ const candidates = copilotMcpCandidates(env);
141
+ const existing = pickExistingFile(candidates);
142
+ const hasCliDir = fs.existsSync(path.join(home, ".copilot"));
143
+ const installed = Boolean(existing) ||
144
+ hasCliDir ||
145
+ fs.existsSync(path.join(home, ".github")) ||
146
+ fs.existsSync(path.join(home, ".config", "github-copilot")) ||
147
+ fs.existsSync(path.join(home, "Library", "Application Support", "github-copilot")) ||
148
+ fs.existsSync(path.join(home, "AppData", "Roaming", "github-copilot")) ||
149
+ commandExists("gh");
150
+ return { installed, existing, cliConfig: candidates[0], hasCliDir };
151
+ }
152
+ export function codexJsonCandidates(phrenPath, env = process.env) {
153
+ const home = homeDir(env);
154
+ return [
155
+ joinPortable(home, ".codex", "config.json"),
156
+ joinPortable(home, ".codex", "mcp.json"),
157
+ path.join(phrenPath, "codex.json"),
158
+ ];
159
+ }
160
+ export function resolveCodexMcpConfig(phrenPath, commandExists, env = process.env) {
161
+ const home = homeDir(env);
162
+ const tomlPath = joinPortable(home, ".codex", "config.toml");
163
+ const jsonCandidates = codexJsonCandidates(phrenPath, env);
164
+ const existingJson = pickExistingFile(jsonCandidates);
165
+ const installed = fs.existsSync(tomlPath) ||
166
+ Boolean(existingJson) ||
167
+ fs.existsSync(path.join(home, ".codex")) ||
168
+ commandExists("codex");
169
+ return {
170
+ installed,
171
+ tomlPath,
172
+ existingJson,
173
+ preferToml: fs.existsSync(tomlPath) || !existingJson,
174
+ jsonCandidates,
175
+ };
176
+ }
@@ -0,0 +1,18 @@
1
+ import { resolveActiveProfile } from "./profile-store.js";
2
+ export function requestedProfileFromEnv() {
3
+ const profile = (process.env.PHREN_PROFILE)?.trim();
4
+ return profile ? profile : undefined;
5
+ }
6
+ /**
7
+ * Resolve the effective runtime profile for user-facing entrypoints.
8
+ * Explicit env selection is strict. Implicit selection is best-effort via
9
+ * machines.yaml / profiles and falls back to an unscoped view during early setup.
10
+ */
11
+ export function resolveRuntimeProfile(phrenPath, requestedProfile = requestedProfileFromEnv()) {
12
+ const result = resolveActiveProfile(phrenPath, requestedProfile);
13
+ if (result.ok)
14
+ return result.data || "";
15
+ if (requestedProfile)
16
+ throw new Error(result.error);
17
+ return "";
18
+ }
@@ -0,0 +1,131 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { errorMessage } from "./utils.js";
4
+ import { debugLog, sessionMarker } from "./shared.js";
5
+ import { atomicWriteJson } from "./session-utils.js";
6
+ function sanitizeFileSegment(value) {
7
+ const trimmed = value.trim();
8
+ const safe = trimmed.replace(/[^a-zA-Z0-9._-]+/g, "_");
9
+ return safe || "unknown";
10
+ }
11
+ function checkpointDir(phrenPath) {
12
+ const probe = sessionMarker(phrenPath, "checkpoint-probe.json");
13
+ return path.dirname(probe);
14
+ }
15
+ function checkpointFileName(project, taskId) {
16
+ return `checkpoint-${sanitizeFileSegment(project)}-${sanitizeFileSegment(taskId)}.json`;
17
+ }
18
+ export function checkpointPath(phrenPath, project, taskId) {
19
+ return sessionMarker(phrenPath, checkpointFileName(project, taskId));
20
+ }
21
+ function readCheckpointFile(filePath) {
22
+ try {
23
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
24
+ if (!parsed || typeof parsed !== "object")
25
+ return null;
26
+ if (typeof parsed.project !== "string" || typeof parsed.taskId !== "string" || typeof parsed.taskLine !== "string")
27
+ return null;
28
+ return {
29
+ project: parsed.project,
30
+ taskId: parsed.taskId,
31
+ taskText: typeof parsed.taskText === "string" ? parsed.taskText : parsed.taskLine,
32
+ taskLine: parsed.taskLine,
33
+ sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId : undefined,
34
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : new Date().toISOString(),
35
+ resumptionHint: {
36
+ lastAttempt: parsed.resumptionHint?.lastAttempt && typeof parsed.resumptionHint.lastAttempt === "string"
37
+ ? parsed.resumptionHint.lastAttempt
38
+ : "No prior attempt captured",
39
+ nextStep: parsed.resumptionHint?.nextStep && typeof parsed.resumptionHint.nextStep === "string"
40
+ ? parsed.resumptionHint.nextStep
41
+ : parsed.taskLine,
42
+ },
43
+ gitStatus: typeof parsed.gitStatus === "string" ? parsed.gitStatus : "",
44
+ editedFiles: Array.isArray(parsed.editedFiles) ? parsed.editedFiles.filter((v) => typeof v === "string") : [],
45
+ failingTests: Array.isArray(parsed.failingTests) ? parsed.failingTests.filter((v) => typeof v === "string") : [],
46
+ };
47
+ }
48
+ catch (err) {
49
+ debugLog(`checkpoint read ${filePath}: ${errorMessage(err)}`);
50
+ return null;
51
+ }
52
+ }
53
+ export function writeTaskCheckpoint(phrenPath, checkpoint) {
54
+ const filePath = checkpointPath(phrenPath, checkpoint.project, checkpoint.taskId);
55
+ const normalizedCheckpoint = {
56
+ ...checkpoint,
57
+ taskText: checkpoint.taskText ?? checkpoint.taskLine,
58
+ };
59
+ atomicWriteJson(filePath, normalizedCheckpoint);
60
+ }
61
+ export function listTaskCheckpoints(phrenPath, project) {
62
+ const dir = checkpointDir(phrenPath);
63
+ let files;
64
+ try {
65
+ files = fs.readdirSync(dir);
66
+ }
67
+ catch {
68
+ return [];
69
+ }
70
+ const rows = [];
71
+ for (const name of files) {
72
+ if (!name.startsWith("checkpoint-") || !name.endsWith(".json"))
73
+ continue;
74
+ const filePath = path.join(dir, name);
75
+ const parsed = readCheckpointFile(filePath);
76
+ if (!parsed)
77
+ continue;
78
+ if (project && parsed.project !== project)
79
+ continue;
80
+ let mtimeMs = 0;
81
+ try {
82
+ mtimeMs = fs.statSync(filePath).mtimeMs;
83
+ }
84
+ catch {
85
+ // keep 0
86
+ }
87
+ rows.push({ checkpoint: parsed, mtimeMs });
88
+ }
89
+ rows.sort((a, b) => b.mtimeMs - a.mtimeMs);
90
+ return rows.map((row) => row.checkpoint);
91
+ }
92
+ export function clearTaskCheckpoint(phrenPath, args) {
93
+ const ids = new Set();
94
+ if (args.taskId)
95
+ ids.add(args.taskId);
96
+ if (args.stableId)
97
+ ids.add(args.stableId);
98
+ if (args.positionalId)
99
+ ids.add(args.positionalId);
100
+ let removed = 0;
101
+ for (const id of ids) {
102
+ const filePath = checkpointPath(phrenPath, args.project, id);
103
+ try {
104
+ if (fs.existsSync(filePath)) {
105
+ fs.unlinkSync(filePath);
106
+ removed++;
107
+ }
108
+ }
109
+ catch (err) {
110
+ debugLog(`checkpoint clear ${filePath}: ${errorMessage(err)}`);
111
+ }
112
+ }
113
+ const allProjectCheckpoints = listTaskCheckpoints(phrenPath, args.project);
114
+ for (const checkpoint of allProjectCheckpoints) {
115
+ const idMatch = ids.size > 0 && ids.has(checkpoint.taskId);
116
+ const lineMatch = args.taskLine && checkpoint.taskLine === args.taskLine;
117
+ if (!idMatch && !lineMatch)
118
+ continue;
119
+ const filePath = checkpointPath(phrenPath, checkpoint.project, checkpoint.taskId);
120
+ try {
121
+ if (fs.existsSync(filePath)) {
122
+ fs.unlinkSync(filePath);
123
+ removed++;
124
+ }
125
+ }
126
+ catch (err) {
127
+ debugLog(`checkpoint clear scan ${filePath}: ${errorMessage(err)}`);
128
+ }
129
+ }
130
+ return removed;
131
+ }
@@ -0,0 +1,68 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ /**
4
+ * Write JSON to a file atomically using temp-file + rename.
5
+ * Ensures the parent directory exists before writing.
6
+ */
7
+ export function atomicWriteJson(filePath, data) {
8
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
9
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
10
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n");
11
+ fs.renameSync(tmpPath, filePath);
12
+ }
13
+ /**
14
+ * Log an error to stderr when PHREN_DEBUG is enabled.
15
+ * Centralises the repeated `if (PHREN_DEBUG) stderr.write(...)` pattern.
16
+ */
17
+ export function debugError(scope, err) {
18
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG)) {
19
+ process.stderr.write(`[phren] ${scope}: ${err instanceof Error ? err.message : String(err)}\n`);
20
+ }
21
+ }
22
+ /**
23
+ * Enumerate all `session-*.json` files under `dir`, parse each one via `parse`,
24
+ * and keep entries where `filter` returns true.
25
+ *
26
+ * Returns an array of `{ fullPath, data, mtimeMs }` sorted newest-mtime-first.
27
+ * `includeMtime` controls whether `fs.statSync` is called (some callers don't need it).
28
+ */
29
+ export function scanSessionFiles(dir, parse, filter, opts) {
30
+ const includeMtime = opts?.includeMtime ?? true;
31
+ const errorScope = opts?.errorScope ?? "scanSessionFiles";
32
+ let entries;
33
+ try {
34
+ entries = fs.readdirSync(dir, { withFileTypes: true });
35
+ }
36
+ catch (err) {
37
+ debugError(`${errorScope} readdir`, err);
38
+ return [];
39
+ }
40
+ const results = [];
41
+ for (const entry of entries) {
42
+ if (!entry.isFile() || !entry.name.startsWith("session-") || !entry.name.endsWith(".json"))
43
+ continue;
44
+ const fullPath = path.join(dir, entry.name);
45
+ try {
46
+ const data = parse(fullPath);
47
+ if (data === null)
48
+ continue;
49
+ if (!filter(data, fullPath))
50
+ continue;
51
+ let mtimeMs = 0;
52
+ if (includeMtime) {
53
+ try {
54
+ mtimeMs = fs.statSync(fullPath).mtimeMs;
55
+ }
56
+ catch { /* keep 0 */ }
57
+ }
58
+ results.push({ fullPath, data, mtimeMs });
59
+ }
60
+ catch (err) {
61
+ debugError(`${errorScope} entry`, err);
62
+ }
63
+ }
64
+ if (includeMtime) {
65
+ results.sort((a, b) => b.mtimeMs - a.mtimeMs);
66
+ }
67
+ return results;
68
+ }
@@ -0,0 +1,8 @@
1
+ // Barrel re-export. Internal code imports from the specific modules directly.
2
+ export { checkConsolidationNeeded, validateFindingsFormat, stripTaskDoneSection, validateTaskFormat, extractConflictVersions, mergeFindings, mergeTask, autoMergeConflicts, } from "./content-validate.js";
3
+ export { filterTrustedFindings, filterTrustedFindingsDetailed, } from "./content-citation.js";
4
+ export { scanForSecrets, resolveCoref, isDuplicateFinding, detectConflicts, extractDynamicEntities, checkSemanticDedup, checkSemanticConflicts, } from "./content-dedup.js";
5
+ export { countActiveFindings, autoArchiveToReference, } from "./content-archive.js";
6
+ export { upsertCanonical, addFindingToFile, addFindingsToFile, } from "./content-learning.js";
7
+ export { FINDING_LIFECYCLE_STATUSES, parseFindingLifecycle, buildLifecycleComments, isInactiveFindingLine, } from "./finding-lifecycle.js";
8
+ export { METADATA_REGEX, parseStatus, parseStatusField, parseSupersession, parseSupersedesRef, parseContradiction, parseAllContradictions, parseFindingId, parseCreatedDate, isCitationLine, isArchiveStart, isArchiveEnd, stripLifecycleMetadata, stripRelationMetadata, stripAllMetadata, stripComments, addMetadata, } from "./content-metadata.js";
@@ -0,0 +1,143 @@
1
+ import * as fs from "fs";
2
+ import * as crypto from "crypto";
3
+ import { runtimeFile, debugLog } from "./shared.js";
4
+ import { withFileLock } from "./shared-governance.js";
5
+ import { errorMessage } from "./utils.js";
6
+ function isEmbeddingEntry(value) {
7
+ if (!value || typeof value !== "object" || Array.isArray(value))
8
+ return false;
9
+ const candidate = value;
10
+ return typeof candidate.model === "string"
11
+ && Array.isArray(candidate.vec)
12
+ && candidate.vec.every((n) => typeof n === "number" && Number.isFinite(n))
13
+ && typeof candidate.at === "string";
14
+ }
15
+ function readEmbeddingMapFromDisk(filePath) {
16
+ const raw = fs.readFileSync(filePath, "utf-8");
17
+ const parsed = JSON.parse(raw);
18
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
19
+ throw new Error("Embedding cache must be a JSON object");
20
+ }
21
+ const data = {};
22
+ for (const [k, v] of Object.entries(parsed)) {
23
+ if (isEmbeddingEntry(v))
24
+ data[k] = v;
25
+ }
26
+ return data;
27
+ }
28
+ export class EmbeddingCache {
29
+ phrenPath;
30
+ cache = new Map();
31
+ dirty = false;
32
+ dirtyUpserts = new Set();
33
+ dirtyDeletes = new Set();
34
+ constructor(phrenPath) {
35
+ this.phrenPath = phrenPath;
36
+ }
37
+ async load() {
38
+ if (this.dirty)
39
+ return;
40
+ const filePath = runtimeFile(this.phrenPath, "embeddings.json");
41
+ try {
42
+ const data = readEmbeddingMapFromDisk(filePath);
43
+ this.cache = new Map(Object.entries(data));
44
+ debugLog(`EmbeddingCache loaded: ${this.cache.size} entries`);
45
+ }
46
+ catch (err) {
47
+ const code = err instanceof Error && "code" in err ? String(err.code ?? "") : "";
48
+ if (code === "ENOENT") {
49
+ this.cache.clear();
50
+ return;
51
+ }
52
+ debugLog(`EmbeddingCache load failed for ${filePath}: ${errorMessage(err)}`);
53
+ }
54
+ }
55
+ get(docPath, model) {
56
+ const entry = this.cache.get(docPath);
57
+ if (!entry || entry.model !== model)
58
+ return null;
59
+ return entry.vec;
60
+ }
61
+ set(docPath, model, vec) {
62
+ this.cache.set(docPath, { model, vec, at: new Date().toISOString().slice(0, 10) });
63
+ this.dirty = true;
64
+ this.dirtyUpserts.add(docPath);
65
+ this.dirtyDeletes.delete(docPath);
66
+ }
67
+ delete(docPath) {
68
+ this.cache.delete(docPath);
69
+ this.dirty = true;
70
+ this.dirtyDeletes.add(docPath);
71
+ this.dirtyUpserts.delete(docPath);
72
+ }
73
+ async flush() {
74
+ if (!this.dirty)
75
+ return;
76
+ const filePath = runtimeFile(this.phrenPath, "embeddings.json");
77
+ try {
78
+ withFileLock(filePath, () => {
79
+ let data = {};
80
+ try {
81
+ if (fs.existsSync(filePath))
82
+ data = readEmbeddingMapFromDisk(filePath);
83
+ }
84
+ catch (err) {
85
+ debugLog(`EmbeddingCache flush merge read failed for ${filePath}: ${errorMessage(err)}`);
86
+ }
87
+ for (const key of this.dirtyDeletes)
88
+ delete data[key];
89
+ for (const key of this.dirtyUpserts) {
90
+ const entry = this.cache.get(key);
91
+ if (entry)
92
+ data[key] = entry;
93
+ }
94
+ const tmp = filePath + `.tmp-${crypto.randomUUID()}`;
95
+ fs.writeFileSync(tmp, JSON.stringify(data));
96
+ fs.renameSync(tmp, filePath);
97
+ this.cache = new Map(Object.entries(data));
98
+ });
99
+ this.dirty = false;
100
+ this.dirtyUpserts.clear();
101
+ this.dirtyDeletes.clear();
102
+ }
103
+ catch (err) {
104
+ debugLog(`EmbeddingCache flush error: ${errorMessage(err)}`);
105
+ }
106
+ }
107
+ getAllEntries() {
108
+ return [...this.cache.entries()].map(([p, e]) => ({ path: p, vec: e.vec, model: e.model }));
109
+ }
110
+ size() {
111
+ return this.cache.size;
112
+ }
113
+ coverage(allPaths) {
114
+ const total = allPaths.length;
115
+ const embedded = allPaths.filter(p => this.cache.has(p)).length;
116
+ const missing = Math.max(0, total - embedded);
117
+ const pct = total === 0 ? 0 : Math.round((embedded / total) * 100);
118
+ const missingPct = total === 0 ? 0 : Math.max(0, 100 - pct);
119
+ const state = total === 0
120
+ ? "empty"
121
+ : embedded === 0
122
+ ? "cold"
123
+ : embedded === total
124
+ ? "warm"
125
+ : "warming";
126
+ return { total, embedded, missing, pct, missingPct, state };
127
+ }
128
+ }
129
+ export function formatEmbeddingCoverage(coverage) {
130
+ if (coverage.total === 0)
131
+ return "0 indexed docs";
132
+ return `${coverage.embedded}/${coverage.total} docs embedded (${coverage.pct}% warm, ${coverage.missingPct}% cold)`;
133
+ }
134
+ // Module-level singleton per phrenPath
135
+ const cacheInstances = new Map();
136
+ export function getEmbeddingCache(phrenPath) {
137
+ const existing = cacheInstances.get(phrenPath);
138
+ if (existing)
139
+ return existing;
140
+ const instance = new EmbeddingCache(phrenPath);
141
+ cacheInstances.set(phrenPath, instance);
142
+ return instance;
143
+ }