@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,279 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { getProjectDirs } from "./shared.js";
4
+ import { parseSkillFrontmatter } from "./link-skills.js";
5
+ import { isSkillEnabled } from "./skill-state.js";
6
+ function normalizeCommand(raw, fallbackName) {
7
+ const value = typeof raw === "string" && raw.trim() ? raw.trim() : `/${fallbackName}`;
8
+ return value.startsWith("/") ? value : `/${value}`;
9
+ }
10
+ function normalizeAliases(raw) {
11
+ if (!Array.isArray(raw))
12
+ return [];
13
+ const seen = new Set();
14
+ const aliases = [];
15
+ for (const value of raw) {
16
+ if (typeof value !== "string" || !value.trim())
17
+ continue;
18
+ const normalized = value.trim().startsWith("/") ? value.trim() : `/${value.trim()}`;
19
+ const key = normalized.toLowerCase();
20
+ if (seen.has(key))
21
+ continue;
22
+ seen.add(key);
23
+ aliases.push(normalized);
24
+ }
25
+ return aliases;
26
+ }
27
+ function collectSkills(phrenPath, root, sourceLabel, scopeType, sourceKind, seen) {
28
+ if (!fs.existsSync(root))
29
+ return [];
30
+ const results = [];
31
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
32
+ const isFolder = entry.isDirectory();
33
+ const filePath = isFolder
34
+ ? path.join(root, entry.name, "SKILL.md")
35
+ : entry.name.endsWith(".md") ? path.join(root, entry.name) : null;
36
+ if (!filePath || seen.has(filePath) || !fs.existsSync(filePath))
37
+ continue;
38
+ seen.add(filePath);
39
+ const name = isFolder ? entry.name : entry.name.replace(/\.md$/, "");
40
+ const { frontmatter } = parseSkillFrontmatter(fs.readFileSync(filePath, "utf8"));
41
+ results.push({
42
+ name,
43
+ source: sourceLabel,
44
+ scopeType,
45
+ sourceKind,
46
+ format: isFolder ? "folder" : "flat",
47
+ path: filePath,
48
+ root: isFolder ? path.dirname(filePath) : filePath,
49
+ description: frontmatter?.description,
50
+ enabled: isSkillEnabled(phrenPath, sourceLabel, name),
51
+ command: normalizeCommand(frontmatter?.command, name),
52
+ aliases: normalizeAliases(frontmatter?.aliases),
53
+ });
54
+ }
55
+ return results;
56
+ }
57
+ function getGlobalSkills(phrenPath) {
58
+ const seen = new Set();
59
+ return collectSkills(phrenPath, path.join(phrenPath, "global", "skills"), "global", "global", "canonical", seen);
60
+ }
61
+ function getProjectLocalSkills(phrenPath, project) {
62
+ const seen = new Set();
63
+ const projectDir = path.join(phrenPath, project);
64
+ return collectSkills(phrenPath, path.join(projectDir, "skills"), project, "project", "canonical", seen);
65
+ }
66
+ function skillPriority(skill) {
67
+ if (skill.scopeType === "project" && skill.sourceKind === "canonical")
68
+ return 400;
69
+ if (skill.scopeType === "global" && skill.sourceKind === "canonical")
70
+ return 200;
71
+ return 100;
72
+ }
73
+ function choosePreferredSkill(skills) {
74
+ return [...skills].sort((left, right) => {
75
+ const priority = skillPriority(right) - skillPriority(left);
76
+ if (priority !== 0)
77
+ return priority;
78
+ return left.path.localeCompare(right.path);
79
+ })[0];
80
+ }
81
+ function buildResolvedSkills(raw, mirrorDir) {
82
+ const grouped = new Map();
83
+ for (const skill of raw) {
84
+ const key = skill.name.toLowerCase();
85
+ const bucket = grouped.get(key) || [];
86
+ bucket.push(skill);
87
+ grouped.set(key, bucket);
88
+ }
89
+ const skills = [];
90
+ for (const [key, candidates] of grouped.entries()) {
91
+ const chosen = choosePreferredSkill(candidates);
92
+ const overrides = candidates
93
+ .filter((candidate) => candidate.path !== chosen.path)
94
+ .map((candidate) => ({
95
+ source: candidate.source,
96
+ path: candidate.path,
97
+ sourceKind: candidate.sourceKind,
98
+ }));
99
+ const destName = chosen.format === "folder" ? chosen.name : path.basename(chosen.path);
100
+ skills.push({
101
+ path: chosen.path,
102
+ format: chosen.format,
103
+ root: chosen.root,
104
+ name: chosen.name,
105
+ source: chosen.source,
106
+ enabled: chosen.enabled,
107
+ description: chosen.description,
108
+ command: chosen.command,
109
+ aliases: chosen.aliases,
110
+ scopeType: chosen.scopeType,
111
+ sourceKind: chosen.sourceKind,
112
+ visibleToAgents: chosen.enabled,
113
+ commandRegistered: true,
114
+ overrides,
115
+ mirrorTargets: mirrorDir ? [path.join(mirrorDir, destName)] : [],
116
+ });
117
+ grouped.delete(key);
118
+ }
119
+ skills.sort((left, right) => left.name.localeCompare(right.name));
120
+ const commandOwners = new Map();
121
+ for (const skill of skills) {
122
+ if (!skill.visibleToAgents)
123
+ continue;
124
+ for (const command of [skill.command, ...skill.aliases]) {
125
+ const key = command.toLowerCase();
126
+ const owners = commandOwners.get(key) || [];
127
+ owners.push({ skillId: skill.name, command });
128
+ commandOwners.set(key, owners);
129
+ }
130
+ }
131
+ const problems = [];
132
+ const collisionKeys = new Set();
133
+ for (const [key, owners] of commandOwners.entries()) {
134
+ if (owners.length < 2)
135
+ continue;
136
+ collisionKeys.add(key);
137
+ problems.push({
138
+ code: "command-collision",
139
+ command: owners[0]?.command,
140
+ skillIds: owners.map((owner) => owner.skillId),
141
+ message: `Command ${owners[0]?.command || key} resolves ambiguously across ${owners.map((owner) => owner.skillId).join(", ")}.`,
142
+ });
143
+ }
144
+ const commands = [];
145
+ for (const skill of skills) {
146
+ const commandEntries = [
147
+ { command: skill.command, kind: "primary" },
148
+ ...skill.aliases.map((alias) => ({ command: alias, kind: "alias" })),
149
+ ];
150
+ skill.commandRegistered = skill.visibleToAgents && !collisionKeys.has(skill.command.toLowerCase());
151
+ for (const entry of commandEntries) {
152
+ commands.push({
153
+ command: entry.command,
154
+ type: "skill",
155
+ skillId: skill.name,
156
+ source: skill.source,
157
+ path: skill.path,
158
+ kind: entry.kind,
159
+ registered: skill.visibleToAgents && !collisionKeys.has(entry.command.toLowerCase()),
160
+ });
161
+ }
162
+ }
163
+ return {
164
+ scope: "global",
165
+ generatedAt: new Date().toISOString(),
166
+ skills,
167
+ commands,
168
+ problems,
169
+ };
170
+ }
171
+ function toResolvedSkill(skill) {
172
+ return {
173
+ path: skill.path,
174
+ format: skill.format,
175
+ root: skill.root,
176
+ name: skill.name,
177
+ source: skill.source,
178
+ enabled: skill.enabled,
179
+ description: skill.description,
180
+ command: skill.command,
181
+ aliases: skill.aliases,
182
+ scopeType: skill.scopeType,
183
+ sourceKind: skill.sourceKind,
184
+ visibleToAgents: skill.enabled,
185
+ commandRegistered: skill.enabled,
186
+ overrides: [],
187
+ mirrorTargets: [],
188
+ };
189
+ }
190
+ export function getAllSkills(phrenPath, profile) {
191
+ const all = getGlobalSkills(phrenPath);
192
+ for (const dir of getProjectDirs(phrenPath, profile)) {
193
+ const source = path.basename(dir);
194
+ if (source === "global")
195
+ continue;
196
+ all.push(...getProjectLocalSkills(phrenPath, source));
197
+ }
198
+ return all;
199
+ }
200
+ export function getLocalSkills(phrenPath, scope) {
201
+ if (scope.toLowerCase() === "global")
202
+ return getGlobalSkills(phrenPath);
203
+ return getProjectLocalSkills(phrenPath, scope);
204
+ }
205
+ export function buildSkillManifest(phrenPath, profile, scope, mirrorDir) {
206
+ const manifest = scope.toLowerCase() === "global"
207
+ ? buildResolvedSkills(getGlobalSkills(phrenPath), mirrorDir)
208
+ : buildResolvedSkills([...getGlobalSkills(phrenPath), ...getProjectLocalSkills(phrenPath, scope)], mirrorDir);
209
+ manifest.scope = scope;
210
+ manifest.project = scope.toLowerCase() === "global" ? undefined : scope;
211
+ manifest.generatedAt = new Date().toISOString();
212
+ return manifest;
213
+ }
214
+ export function getScopedSkills(phrenPath, profile, project) {
215
+ if (!project)
216
+ return getAllSkills(phrenPath, profile).map(toResolvedSkill);
217
+ return buildSkillManifest(phrenPath, profile, project).skills;
218
+ }
219
+ export function findLocalSkill(phrenPath, scope, name) {
220
+ const needle = name.replace(/\.md$/i, "").toLowerCase();
221
+ const matches = getLocalSkills(phrenPath, scope).filter((skill) => skill.name.toLowerCase() === needle);
222
+ if (matches.length === 0)
223
+ return null;
224
+ return toResolvedSkill(choosePreferredSkill(matches));
225
+ }
226
+ export function findSkill(phrenPath, profile, project, name) {
227
+ const needle = name.replace(/\.md$/i, "").toLowerCase();
228
+ if (project) {
229
+ const matches = buildSkillManifest(phrenPath, profile, project).skills.filter((skill) => skill.name.toLowerCase() === needle);
230
+ return matches[0] || null;
231
+ }
232
+ const matches = getAllSkills(phrenPath, profile).filter((skill) => skill.name.toLowerCase() === needle);
233
+ if (matches.length === 0)
234
+ return null;
235
+ if (matches.length > 1) {
236
+ return { error: `Skill '${name}' exists in multiple scopes: ${matches.map((match) => match.source).join(", ")}. Pass project= to disambiguate.` };
237
+ }
238
+ return toResolvedSkill(matches[0]);
239
+ }
240
+ export function renderSkillInstructionsSection(manifest) {
241
+ const visible = manifest.skills.filter((skill) => skill.visibleToAgents);
242
+ const disabled = manifest.skills.filter((skill) => !skill.visibleToAgents);
243
+ const lines = [
244
+ "<!-- phren:generated-skills -->",
245
+ "## Available Phren skills",
246
+ "",
247
+ "These skills are resolved from Phren source files and mirrored into `.claude/skills/` for agent discovery.",
248
+ "",
249
+ ];
250
+ if (!visible.length) {
251
+ lines.push("No enabled skills are resolved for this scope.");
252
+ }
253
+ else {
254
+ lines.push("| Command | Skill | Scope | Source |");
255
+ lines.push("| --- | --- | --- | --- |");
256
+ for (const skill of visible) {
257
+ lines.push(`| \`${skill.command}\` | \`${skill.name}\` | \`${skill.source}\` | \`${skill.path}\` |`);
258
+ }
259
+ }
260
+ if (disabled.length) {
261
+ lines.push("");
262
+ lines.push("Disabled skills:");
263
+ for (const skill of disabled) {
264
+ lines.push(`- \`${skill.name}\` (${skill.source})`);
265
+ }
266
+ }
267
+ if (manifest.problems.length) {
268
+ lines.push("");
269
+ lines.push("Skill registry problems:");
270
+ for (const problem of manifest.problems) {
271
+ lines.push(`- ${problem.message}`);
272
+ }
273
+ }
274
+ lines.push("");
275
+ lines.push("Generated artifacts:");
276
+ lines.push("- `.claude/skill-manifest.json`");
277
+ lines.push("- `.claude/skill-commands.json`");
278
+ return lines.join("\n");
279
+ }
@@ -0,0 +1,28 @@
1
+ import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
2
+ function skillStateKey(scope, name) {
3
+ return `${scope}:${name.replace(/\.md$/i, "").trim().toLowerCase()}`;
4
+ }
5
+ function readDisabledSkillMap(phrenPath) {
6
+ const prefs = readInstallPreferences(phrenPath);
7
+ return prefs.disabledSkills && typeof prefs.disabledSkills === "object"
8
+ ? { ...prefs.disabledSkills }
9
+ : {};
10
+ }
11
+ export function isSkillEnabled(phrenPath, scope, name) {
12
+ const disabled = readDisabledSkillMap(phrenPath);
13
+ return disabled[skillStateKey(scope, name)] !== true;
14
+ }
15
+ export function setSkillEnabled(phrenPath, scope, name, enabled) {
16
+ const disabled = readDisabledSkillMap(phrenPath);
17
+ const key = skillStateKey(scope, name);
18
+ if (enabled)
19
+ delete disabled[key];
20
+ else
21
+ disabled[key] = true;
22
+ writeInstallPreferences(phrenPath, {
23
+ disabledSkills: Object.keys(disabled).length ? disabled : undefined,
24
+ });
25
+ }
26
+ export function getSkillStateKey(scope, name) {
27
+ return skillStateKey(scope, name);
28
+ }
@@ -0,0 +1,57 @@
1
+ import { debugLog } from "./shared.js";
2
+ import { decodeStringRow, queryRows } from "./shared-index.js";
3
+ import { errorMessage } from "./utils.js";
4
+ /** Throttle delay between embedding requests in the background embed loop. */
5
+ const BACKGROUND_EMBED_THROTTLE_MS = 50;
6
+ async function loadWarmupDeps() {
7
+ const { checkOllamaAvailable, embedText, getEmbeddingModel, getOllamaUrl } = await import("./shared-ollama.js");
8
+ return {
9
+ checkOllamaAvailable,
10
+ embedText,
11
+ getEmbeddingModel,
12
+ getOllamaUrl,
13
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
14
+ };
15
+ }
16
+ export function startEmbeddingWarmup(db, cache, deps) {
17
+ const loadPromise = cache.load().catch((err) => {
18
+ debugLog(`Embedding cache startup load failed: ${errorMessage(err)}`);
19
+ });
20
+ const backgroundPromise = backgroundEmbedMissingDocs(db, cache, deps);
21
+ return { loadPromise, backgroundPromise };
22
+ }
23
+ export async function backgroundEmbedMissingDocs(db, cache, deps) {
24
+ try {
25
+ const resolved = { ...(await loadWarmupDeps()), ...deps };
26
+ if (!resolved.getOllamaUrl())
27
+ return 0;
28
+ if (!await resolved.checkOllamaAvailable())
29
+ return 0;
30
+ const rows = queryRows(db, "SELECT path, content FROM docs", []);
31
+ if (!rows)
32
+ return 0;
33
+ const model = resolved.getEmbeddingModel();
34
+ let count = 0;
35
+ for (const row of rows) {
36
+ const [docPath, content] = decodeStringRow(row, 2, "backgroundEmbedMissingDocs");
37
+ if (cache.get(docPath, model))
38
+ continue;
39
+ const vec = await resolved.embedText(content.slice(0, 8000));
40
+ if (vec) {
41
+ cache.set(docPath, model, vec);
42
+ count++;
43
+ if (count % 10 === 0)
44
+ await cache.flush();
45
+ }
46
+ await resolved.sleep(BACKGROUND_EMBED_THROTTLE_MS);
47
+ }
48
+ if (count > 0)
49
+ await cache.flush();
50
+ debugLog(`backgroundEmbedMissingDocs: embedded ${count} new docs`);
51
+ return count;
52
+ }
53
+ catch (err) {
54
+ debugLog(`backgroundEmbedMissingDocs error: ${errorMessage(err)}`);
55
+ return 0;
56
+ }
57
+ }
@@ -0,0 +1,323 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { findPhrenPath, getProjectDirs, EXEC_TIMEOUT_QUICK_MS, debugLog, isRecord, hookConfigPath, homeDir, readRootManifest, } from "./shared.js";
5
+ import { buildIndex, detectProject, findFtsCacheForPath, listIndexedDocumentPaths, queryRows } from "./shared-index.js";
6
+ import { getMcpEnabledPreference, getHooksEnabledPreference } from "./init.js";
7
+ import { getTelemetrySummary } from "./telemetry.js";
8
+ import { runGit as runGitShared } from "./utils.js";
9
+ import { readRuntimeHealth, resolveTaskFilePath } from "./data-access.js";
10
+ import { resolveRuntimeProfile } from "./runtime-profile.js";
11
+ import { renderPhrenArt } from "./phren-art.js";
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ function readPackageVersion() {
14
+ try {
15
+ const pkgPath = path.resolve(__dirname, "..", "..", "package.json");
16
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
17
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
18
+ }
19
+ catch (err) {
20
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
21
+ process.stderr.write(`[phren] readPackageVersion: ${err instanceof Error ? err.message : String(err)}\n`);
22
+ return "unknown";
23
+ }
24
+ }
25
+ const RESET = "\x1b[0m";
26
+ const BOLD = "\x1b[1m";
27
+ const DIM = "\x1b[2m";
28
+ const GREEN = "\x1b[32m";
29
+ const YELLOW = "\x1b[33m";
30
+ const RED = "\x1b[31m";
31
+ const CYAN = "\x1b[36m";
32
+ function check(ok) {
33
+ return ok ? `${GREEN}ok${RESET}` : `${RED}missing${RESET}`;
34
+ }
35
+ function countBullets(filePath) {
36
+ if (!fs.existsSync(filePath))
37
+ return 0;
38
+ const content = fs.readFileSync(filePath, "utf8");
39
+ return content.split("\n").filter((l) => l.startsWith("- ")).length;
40
+ }
41
+ function countQueueItems(phrenPath, project) {
42
+ const queueFile = path.join(phrenPath, project, "MEMORY_QUEUE.md");
43
+ return countBullets(queueFile);
44
+ }
45
+ function runGit(cwd, args) {
46
+ return runGitShared(cwd, args, EXEC_TIMEOUT_QUICK_MS, debugLog);
47
+ }
48
+ function hasCommandHook(value) {
49
+ if (!Array.isArray(value))
50
+ return false;
51
+ return value.some((entry) => {
52
+ if (!isRecord(entry) || !Array.isArray(entry.hooks))
53
+ return false;
54
+ return entry.hooks.some((hook) => isRecord(hook) && typeof hook.command === "string" && (hook.command.includes("phren") || hook.command.includes("phren")));
55
+ });
56
+ }
57
+ export async function runStatus() {
58
+ const phrenPath = findPhrenPath();
59
+ if (!phrenPath) {
60
+ console.log(`${RED}phren not found${RESET}. Run ${CYAN}npx phren init${RESET} to set up.`);
61
+ process.exit(1);
62
+ }
63
+ const cwd = process.cwd();
64
+ const manifest = readRootManifest(phrenPath);
65
+ const profile = resolveRuntimeProfile(phrenPath);
66
+ const activeProject = detectProject(phrenPath, cwd, profile);
67
+ const version = readPackageVersion();
68
+ console.log("");
69
+ console.log(renderPhrenArt(" "));
70
+ console.log(`\n${BOLD}phren${RESET} ${DIM}v${version}${RESET}\n`);
71
+ // Active project
72
+ if (activeProject) {
73
+ console.log(` ${DIM}project${RESET} ${activeProject}`);
74
+ }
75
+ // Phren path and config
76
+ console.log(` ${DIM}path${RESET} ${phrenPath}`);
77
+ console.log(` ${DIM}mode${RESET} ${manifest?.installMode || "unknown"}`);
78
+ if (manifest?.workspaceRoot) {
79
+ console.log(` ${DIM}workspace${RESET} ${manifest.workspaceRoot}`);
80
+ }
81
+ if (manifest?.syncMode) {
82
+ console.log(` ${DIM}sync${RESET} ${manifest.syncMode}`);
83
+ }
84
+ if (profile) {
85
+ console.log(` ${DIM}profile${RESET} ${profile}`);
86
+ }
87
+ // MCP + hooks status
88
+ const mcpEnabled = getMcpEnabledPreference(phrenPath);
89
+ const hooksEnabled = getHooksEnabledPreference(phrenPath);
90
+ console.log(` ${DIM}mcp${RESET} ${mcpEnabled ? `${GREEN}on${RESET}` : `${YELLOW}off${RESET}`}`);
91
+ console.log(` ${DIM}hooks${RESET} ${hooksEnabled ? `${GREEN}on${RESET}` : `${YELLOW}off${RESET}`}`);
92
+ // Hook health: check ~/.claude/settings.json
93
+ let hooksInstalled = false;
94
+ let mcpConfigured = false;
95
+ if (manifest?.installMode === "project-local" && manifest.workspaceRoot) {
96
+ const workspaceMcp = path.join(manifest.workspaceRoot, ".vscode", "mcp.json");
97
+ if (fs.existsSync(workspaceMcp)) {
98
+ try {
99
+ const parsed = JSON.parse(fs.readFileSync(workspaceMcp, "utf8"));
100
+ const settings = isRecord(parsed) ? parsed : {};
101
+ const servers = isRecord(settings.servers) ? settings.servers : undefined;
102
+ mcpConfigured = Boolean(servers?.phren || servers?.phren);
103
+ }
104
+ catch (err) {
105
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
106
+ process.stderr.write(`[phren] statusWorkspaceMcp parse: ${err instanceof Error ? err.message : String(err)}\n`);
107
+ }
108
+ }
109
+ }
110
+ else {
111
+ const settingsPath = hookConfigPath("claude");
112
+ if (fs.existsSync(settingsPath)) {
113
+ try {
114
+ const parsed = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
115
+ const settings = isRecord(parsed) ? parsed : {};
116
+ const mcpServers = isRecord(settings.mcpServers) ? settings.mcpServers : undefined;
117
+ const hooks = isRecord(settings.hooks) ? settings.hooks : undefined;
118
+ mcpConfigured = Boolean(mcpServers?.phren || mcpServers?.phren);
119
+ const hookEvents = ["UserPromptSubmit", "Stop", "SessionStart"];
120
+ hooksInstalled = hookEvents.every((event) => hasCommandHook(hooks?.[event]));
121
+ }
122
+ catch (err) {
123
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
124
+ process.stderr.write(`[phren] statusHooks settingsParse: ${err instanceof Error ? err.message : String(err)}\n`);
125
+ }
126
+ }
127
+ }
128
+ console.log(` ${DIM}mcp cfg${RESET} ${check(mcpConfigured)} ${DIM}(${manifest?.installMode === "project-local" ? ".vscode/mcp.json" : "settings.json"})${RESET}`);
129
+ if (manifest?.installMode === "project-local") {
130
+ console.log(` ${DIM}hooks cfg${RESET} ${DIM}n/a in project-local mode${RESET}`);
131
+ }
132
+ else {
133
+ console.log(` ${DIM}hooks cfg${RESET} ${check(hooksInstalled)} ${DIM}(settings.json)${RESET}`);
134
+ }
135
+ // FTS index health
136
+ let ftsIndexOk = false;
137
+ let ftsIndexSize = 0;
138
+ let ftsDocCount = null;
139
+ try {
140
+ const cache = findFtsCacheForPath(phrenPath, profile);
141
+ ftsIndexOk = cache.exists;
142
+ ftsIndexSize = cache.sizeBytes ?? 0;
143
+ if (!ftsIndexOk) {
144
+ const db = await buildIndex(phrenPath, profile || undefined);
145
+ const healthRow = queryRows(db, "SELECT count(*) FROM docs", []);
146
+ const count = Number(healthRow?.[0]?.[0] ?? 0);
147
+ if (Number.isFinite(count) && count >= 0) {
148
+ ftsIndexOk = true;
149
+ ftsDocCount = count;
150
+ }
151
+ }
152
+ }
153
+ catch (err) {
154
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
155
+ process.stderr.write(`[phren] statusFtsIndex: ${err instanceof Error ? err.message : String(err)}\n`);
156
+ }
157
+ const ftsLabel = ftsIndexOk
158
+ ? `${GREEN}ok${RESET} ${DIM}(${ftsIndexSize > 0 ? `${(ftsIndexSize / 1024).toFixed(0)} KB` : `${ftsDocCount ?? 0} docs`})${RESET}`
159
+ : `${YELLOW}not built${RESET} ${DIM}(run a search to build)${RESET}`;
160
+ console.log(` ${DIM}fts${RESET} ${ftsLabel}`);
161
+ try {
162
+ const { getOllamaUrl, checkOllamaAvailable, checkModelAvailable, getEmbeddingModel } = await import("./shared-ollama.js");
163
+ const { getEmbeddingCache, formatEmbeddingCoverage } = await import("./shared-embedding-cache.js");
164
+ const ollamaUrl = getOllamaUrl();
165
+ if (!ollamaUrl) {
166
+ console.log(` ${DIM}semantic${RESET} ${DIM}disabled (optional)${RESET}`);
167
+ }
168
+ else {
169
+ const available = await checkOllamaAvailable();
170
+ if (!available) {
171
+ console.log(` ${DIM}semantic${RESET} ${YELLOW}offline${RESET} ${DIM}(${ollamaUrl})${RESET}`);
172
+ }
173
+ else {
174
+ const modelReady = await checkModelAvailable();
175
+ const model = getEmbeddingModel();
176
+ if (!modelReady) {
177
+ console.log(` ${DIM}semantic${RESET} ${YELLOW}model missing${RESET} ${DIM}(${model})${RESET}`);
178
+ }
179
+ else {
180
+ const cache = getEmbeddingCache(phrenPath);
181
+ await cache.load().catch(() => { });
182
+ const coverage = cache.coverage(listIndexedDocumentPaths(phrenPath, profile || undefined));
183
+ console.log(` ${DIM}semantic${RESET} ${GREEN}ready${RESET} ${DIM}(${model}; ${formatEmbeddingCoverage(coverage)})${RESET}`);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ catch (err) {
189
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
190
+ process.stderr.write(`[phren] statusSemantic: ${err instanceof Error ? err.message : String(err)}\n`);
191
+ }
192
+ // Agent integration status
193
+ function hasPhrenEntry(filePath) {
194
+ if (!fs.existsSync(filePath))
195
+ return false;
196
+ try {
197
+ const raw = fs.readFileSync(filePath, "utf8");
198
+ return raw.includes('"phren"') || raw.includes("'phren'") || raw.includes('"phren"') || raw.includes("'phren'");
199
+ }
200
+ catch (err) {
201
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
202
+ process.stderr.write(`[phren] hasPhrenEntry: ${err instanceof Error ? err.message : String(err)}\n`);
203
+ return false;
204
+ }
205
+ }
206
+ function agentConfigured(candidates) {
207
+ return candidates.some(hasPhrenEntry);
208
+ }
209
+ const home = homeDir();
210
+ const agentChecks = manifest?.installMode === "project-local" && manifest.workspaceRoot ? [
211
+ {
212
+ name: "VS Code (workspace)",
213
+ configured: fs.existsSync(path.join(manifest.workspaceRoot, ".vscode", "mcp.json")),
214
+ },
215
+ ] : [
216
+ {
217
+ name: "Claude Code",
218
+ configured: agentConfigured([
219
+ path.join(home, ".claude.json"),
220
+ path.join(home, ".claude", "settings.json"),
221
+ ]),
222
+ },
223
+ {
224
+ name: "Cursor",
225
+ configured: agentConfigured([
226
+ path.join(home, ".cursor", "mcp.json"),
227
+ path.join(home, ".config", "Cursor", "User", "mcp.json"),
228
+ path.join(home, "Library", "Application Support", "Cursor", "User", "mcp.json"),
229
+ ]),
230
+ },
231
+ {
232
+ name: "Copilot CLI",
233
+ configured: agentConfigured([
234
+ path.join(home, ".copilot", "mcp-config.json"),
235
+ path.join(home, ".config", "github-copilot", "mcp.json"),
236
+ path.join(home, "Library", "Application Support", "github-copilot", "mcp.json"),
237
+ ]),
238
+ },
239
+ {
240
+ name: "Codex",
241
+ configured: agentConfigured([
242
+ path.join(home, ".codex", "config.json"),
243
+ path.join(home, ".codex", "mcp.json"),
244
+ path.join(home, ".codex", "config.toml"),
245
+ ]),
246
+ },
247
+ {
248
+ name: "Windsurf",
249
+ configured: agentConfigured([
250
+ path.join(home, ".windsurf", "mcp.json"),
251
+ path.join(home, ".config", "Windsurf", "User", "mcp.json"),
252
+ path.join(home, "Library", "Application Support", "Windsurf", "User", "mcp.json"),
253
+ ]),
254
+ },
255
+ ];
256
+ const configuredAgents = agentChecks.filter((a) => a.configured).map((a) => a.name);
257
+ const missingAgents = agentChecks.filter((a) => !a.configured).map((a) => a.name);
258
+ if (configuredAgents.length > 0) {
259
+ console.log(` ${DIM}agents${RESET} ${GREEN}${configuredAgents.join(", ")}${RESET}`);
260
+ }
261
+ if (missingAgents.length > 0) {
262
+ console.log(` ${DIM} Not configured: ${missingAgents.join(", ")} — run phren init to add${RESET}`);
263
+ }
264
+ // Stats
265
+ const projectDirs = getProjectDirs(phrenPath, profile);
266
+ let totalFindings = 0;
267
+ let totalTask = 0;
268
+ let totalQueue = 0;
269
+ for (const dir of projectDirs) {
270
+ const projName = path.basename(dir);
271
+ totalFindings += countBullets(path.join(phrenPath, projName, "FINDINGS.md"));
272
+ const taskPath = resolveTaskFilePath(phrenPath, projName);
273
+ if (taskPath)
274
+ totalTask += countBullets(taskPath);
275
+ totalQueue += countQueueItems(phrenPath, projName);
276
+ }
277
+ console.log(`\n ${DIM}phren holds${RESET} ${projectDirs.length} projects, ${totalFindings} fragments, ${totalTask} tasks, ${totalQueue} queued`);
278
+ const gitTarget = manifest?.installMode === "project-local" && manifest.workspaceRoot ? manifest.workspaceRoot : phrenPath;
279
+ const isGitRepo = runGit(gitTarget, ["rev-parse", "--is-inside-work-tree"]) === "true";
280
+ const hasOriginRemote = isGitRepo && Boolean(runGit(gitTarget, ["remote", "get-url", "origin"]));
281
+ const runtime = readRuntimeHealth(phrenPath);
282
+ if (manifest?.installMode === "project-local") {
283
+ console.log(`\n ${DIM}sync${RESET} workspace-managed`);
284
+ console.log(` auto-save ${runtime.lastAutoSave?.status || "n/a"}`);
285
+ }
286
+ else if (isGitRepo && !hasOriginRemote) {
287
+ console.log(`\n ${DIM}sync${RESET} local-only ${DIM}(no git remote)${RESET}`);
288
+ console.log(` auto-save ${runtime.lastAutoSave?.status || "n/a"}`);
289
+ console.log(` local commits ${runtime.lastSync?.unsyncedCommits ?? 0}`);
290
+ }
291
+ else {
292
+ console.log(`\n ${DIM}sync${RESET} auto-save ${runtime.lastAutoSave?.status || "n/a"}`);
293
+ console.log(` last pull ${runtime.lastSync?.lastPullStatus || "n/a"}${runtime.lastSync?.lastPullAt ? ` @ ${runtime.lastSync.lastPullAt}` : ""}`);
294
+ console.log(` last push ${runtime.lastSync?.lastPushStatus || "n/a"}${runtime.lastSync?.lastPushAt ? ` @ ${runtime.lastSync.lastPushAt}` : ""}`);
295
+ console.log(` unsynced commits ${runtime.lastSync?.unsyncedCommits ?? 0}`);
296
+ if (runtime.lastSync?.lastPushDetail) {
297
+ console.log(` push detail ${runtime.lastSync.lastPushDetail}`);
298
+ }
299
+ }
300
+ // Recent changes (git log)
301
+ if (isGitRepo) {
302
+ const log = runGit(gitTarget, ["log", "--oneline", "-5", "--no-decorate"]);
303
+ if (log) {
304
+ console.log(`\n ${DIM}recent${RESET}`);
305
+ for (const line of log.split("\n")) {
306
+ console.log(` ${DIM}${line}${RESET}`);
307
+ }
308
+ }
309
+ const dirty = runGit(gitTarget, ["status", "--porcelain"]);
310
+ if (dirty) {
311
+ const count = dirty.split("\n").filter(Boolean).length;
312
+ console.log(` ${YELLOW}${count} uncommitted change(s)${RESET}`);
313
+ }
314
+ }
315
+ else {
316
+ console.log(`\n ${DIM}${gitTarget} is not a git repo${RESET}`);
317
+ }
318
+ // Telemetry
319
+ const telemetry = getTelemetrySummary(phrenPath);
320
+ const firstLine = telemetry.split("\n")[0];
321
+ console.log(`\n ${BOLD}${firstLine}${RESET}`);
322
+ console.log("");
323
+ }