@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,989 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { execFileSync } from "child_process";
4
+ import { expandHomePath, getPhrenPath, getProjectDirs, homePath, hookConfigPath, readRootManifest, } from "./shared.js";
5
+ import { isValidProjectName, errorMessage } from "./utils.js";
6
+ import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
7
+ import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "./skill-registry.js";
8
+ import { setSkillEnabledAndSync, syncSkillLinksForScope } from "./skill-files.js";
9
+ import { findProjectDir } from "./project-locator.js";
10
+ import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask } from "./data-tasks.js";
11
+ import { PROJECT_HOOK_EVENTS, PROJECT_OWNERSHIP_MODES, isProjectHookEnabled, parseProjectOwnershipMode, readProjectConfig, writeProjectConfig, writeProjectHookConfig, } from "./project-config.js";
12
+ import { addFinding, removeFinding } from "./core-finding.js";
13
+ import { supersedeFinding, retractFinding } from "./finding-lifecycle.js";
14
+ import { readCustomHooks, getHookTarget, HOOK_EVENT_VALUES } from "./hooks.js";
15
+ import { runtimeFile } from "./shared.js";
16
+ const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
17
+ function printSkillsUsage() {
18
+ console.log("Usage:");
19
+ console.log(" phren skills list [--project <name>]");
20
+ console.log(" phren skills show <name> [--project <name>]");
21
+ console.log(" phren skills edit <name> [--project <name>]");
22
+ console.log(" phren skills add <project> <path>");
23
+ console.log(" phren skills resolve <project|global> [--json]");
24
+ console.log(" phren skills doctor <project|global>");
25
+ console.log(" phren skills sync <project|global>");
26
+ console.log(" phren skills enable <project|global> <name>");
27
+ console.log(" phren skills disable <project|global> <name>");
28
+ console.log(" phren skills remove <project> <name>");
29
+ }
30
+ function printHooksUsage() {
31
+ console.log("Usage:");
32
+ console.log(" phren hooks list [--project <name>]");
33
+ console.log(" phren hooks show <tool>");
34
+ console.log(" phren hooks edit <tool>");
35
+ console.log(" phren hooks enable <tool>");
36
+ console.log(" phren hooks disable <tool>");
37
+ console.log(" phren hooks add-custom <event> <command>");
38
+ console.log(" phren hooks remove-custom <event> [<command>]");
39
+ console.log(" phren hooks errors [--limit <n>]");
40
+ console.log(" tools: claude|copilot|cursor|codex");
41
+ console.log(" events: " + HOOK_EVENT_VALUES.join(", "));
42
+ }
43
+ function normalizeHookTool(raw) {
44
+ if (!raw)
45
+ return null;
46
+ const tool = raw.toLowerCase();
47
+ return HOOK_TOOLS.includes(tool) ? tool : null;
48
+ }
49
+ function getOptionValue(args, name) {
50
+ const exactIdx = args.indexOf(name);
51
+ if (exactIdx !== -1)
52
+ return args[exactIdx + 1];
53
+ const prefixed = args.find((arg) => arg.startsWith(`${name}=`));
54
+ return prefixed ? prefixed.slice(name.length + 1) : undefined;
55
+ }
56
+ function parseMcpToggle(raw) {
57
+ if (!raw)
58
+ return undefined;
59
+ const normalized = raw.trim().toLowerCase();
60
+ if (normalized === "on" || normalized === "true" || normalized === "enabled")
61
+ return true;
62
+ if (normalized === "off" || normalized === "false" || normalized === "disabled")
63
+ return false;
64
+ return undefined;
65
+ }
66
+ function findSkillPath(name, profile, project) {
67
+ const found = findSkill(getPhrenPath(), profile, project, name);
68
+ if (!found || "error" in found)
69
+ return null;
70
+ return found.path;
71
+ }
72
+ function openInEditor(filePath) {
73
+ const editor = process.env.EDITOR || process.env.VISUAL || "nano";
74
+ try {
75
+ execFileSync(editor, [filePath], { stdio: "inherit" });
76
+ }
77
+ catch (err) {
78
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
79
+ process.stderr.write(`[phren] openInEditor: ${errorMessage(err)}\n`);
80
+ console.error(`Editor "${editor}" failed. Set $EDITOR to your preferred editor.`);
81
+ process.exit(1);
82
+ }
83
+ }
84
+ export function handleSkillsNamespace(args, profile) {
85
+ const subcommand = args[0];
86
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
87
+ printSkillsUsage();
88
+ return;
89
+ }
90
+ if (subcommand === "list") {
91
+ const projectIdx = args.indexOf("--project");
92
+ const project = projectIdx !== -1 ? args[projectIdx + 1] : undefined;
93
+ handleSkillList(profile, project);
94
+ return;
95
+ }
96
+ if (subcommand === "show" || subcommand === "edit") {
97
+ const name = args[1];
98
+ if (!name) {
99
+ printSkillsUsage();
100
+ process.exit(1);
101
+ }
102
+ const projectIdx = args.indexOf("--project");
103
+ const project = projectIdx !== -1 ? args[projectIdx + 1] : undefined;
104
+ const skillPath = findSkillPath(name, profile, project);
105
+ if (!skillPath) {
106
+ console.error(`Skill not found: "${name}"${project ? ` in project "${project}"` : ""}`);
107
+ process.exit(1);
108
+ }
109
+ if (subcommand === "show") {
110
+ console.log(fs.readFileSync(skillPath, "utf8"));
111
+ }
112
+ else {
113
+ openInEditor(skillPath);
114
+ }
115
+ return;
116
+ }
117
+ if (subcommand === "add") {
118
+ const project = args[1];
119
+ const skillPath = args[2];
120
+ if (!project || !skillPath) {
121
+ printSkillsUsage();
122
+ process.exit(1);
123
+ }
124
+ if (!isValidProjectName(project)) {
125
+ console.error(`Invalid project name: "${project}"`);
126
+ process.exit(1);
127
+ }
128
+ const source = path.resolve(expandHomePath(skillPath));
129
+ if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
130
+ console.error(`Skill file not found: ${source}`);
131
+ process.exit(1);
132
+ }
133
+ const baseName = path.basename(source);
134
+ const fileName = baseName.toLowerCase().endsWith(".md") ? baseName : `${baseName}.md`;
135
+ const destDir = path.join(getPhrenPath(), project, "skills");
136
+ const dest = path.join(destDir, fileName);
137
+ fs.mkdirSync(destDir, { recursive: true });
138
+ if (fs.existsSync(dest)) {
139
+ console.error(`Skill already exists: ${dest}`);
140
+ process.exit(1);
141
+ }
142
+ try {
143
+ fs.symlinkSync(source, dest);
144
+ console.log(`Linked skill ${fileName} into ${project}.`);
145
+ }
146
+ catch (err) {
147
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
148
+ process.stderr.write(`[phren] skill add symlinkFailed: ${errorMessage(err)}\n`);
149
+ fs.copyFileSync(source, dest);
150
+ console.log(`Copied skill ${fileName} into ${project}.`);
151
+ }
152
+ return;
153
+ }
154
+ if (subcommand === "resolve" || subcommand === "doctor" || subcommand === "sync") {
155
+ const scope = args[1];
156
+ if (!scope) {
157
+ printSkillsUsage();
158
+ process.exit(1);
159
+ }
160
+ if (scope.toLowerCase() !== "global" && !isValidProjectName(scope)) {
161
+ console.error(`Invalid project name: "${scope}"`);
162
+ process.exit(1);
163
+ }
164
+ if (subcommand === "sync") {
165
+ const syncedManifest = syncSkillLinksForScope(getPhrenPath(), scope);
166
+ if (!syncedManifest) {
167
+ console.error(`Project directory not found for "${scope}".`);
168
+ process.exit(1);
169
+ }
170
+ const mirrorDir = resolveSkillMirrorDir(scope) || homePath(".claude", "skills");
171
+ console.log(`Synced ${syncedManifest.skills.filter((skill) => skill.visibleToAgents).length} skill(s) for ${scope}.`);
172
+ console.log(` ${path.join(path.dirname(mirrorDir), "skill-manifest.json")}`);
173
+ console.log(` ${path.join(path.dirname(mirrorDir), "skill-commands.json")}`);
174
+ return;
175
+ }
176
+ const destDir = resolveSkillMirrorDir(scope);
177
+ const manifest = buildSkillManifest(getPhrenPath(), profile, scope, destDir || undefined);
178
+ if (subcommand === "resolve") {
179
+ if (args.includes("--json")) {
180
+ console.log(JSON.stringify(manifest, null, 2));
181
+ return;
182
+ }
183
+ printResolvedManifest(scope, manifest, destDir);
184
+ return;
185
+ }
186
+ printSkillDoctor(scope, manifest, destDir);
187
+ return;
188
+ }
189
+ if (subcommand === "enable" || subcommand === "disable") {
190
+ const scope = args[1];
191
+ const name = args[2];
192
+ if (!scope || !name) {
193
+ printSkillsUsage();
194
+ process.exit(1);
195
+ }
196
+ if (scope.toLowerCase() !== "global" && !isValidProjectName(scope)) {
197
+ console.error(`Invalid project name: "${scope}"`);
198
+ process.exit(1);
199
+ }
200
+ const resolved = findSkill(getPhrenPath(), profile, scope, name);
201
+ if (!resolved || "error" in resolved) {
202
+ console.error(`Skill not found: "${name}" in "${scope}"`);
203
+ process.exit(1);
204
+ }
205
+ setSkillEnabledAndSync(getPhrenPath(), scope, resolved.name, subcommand === "enable");
206
+ console.log(`${subcommand === "enable" ? "Enabled" : "Disabled"} skill ${resolved.name} in ${scope}.`);
207
+ return;
208
+ }
209
+ if (subcommand === "remove") {
210
+ const project = args[1];
211
+ const name = args[2];
212
+ if (!project || !name) {
213
+ printSkillsUsage();
214
+ process.exit(1);
215
+ }
216
+ if (!isValidProjectName(project)) {
217
+ console.error(`Invalid project name: "${project}"`);
218
+ process.exit(1);
219
+ }
220
+ const resolved = findLocalSkill(getPhrenPath(), project, name)?.path || null;
221
+ if (!resolved) {
222
+ console.error(`Skill not found: "${name}" in project "${project}"`);
223
+ process.exit(1);
224
+ }
225
+ const removePath = path.basename(resolved) === "SKILL.md" ? path.dirname(resolved) : resolved;
226
+ if (fs.statSync(removePath).isDirectory()) {
227
+ fs.rmSync(removePath, { recursive: true, force: true });
228
+ }
229
+ else {
230
+ fs.unlinkSync(removePath);
231
+ }
232
+ console.log(`Removed skill ${name.replace(/\.md$/i, "")} from ${project}.`);
233
+ return;
234
+ }
235
+ console.error(`Unknown skills subcommand: ${subcommand}`);
236
+ printSkillsUsage();
237
+ process.exit(1);
238
+ }
239
+ export function handleHooksNamespace(args) {
240
+ const subcommand = args[0];
241
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
242
+ printHooksUsage();
243
+ return;
244
+ }
245
+ if (subcommand === "list") {
246
+ const phrenPath = getPhrenPath();
247
+ const prefs = readInstallPreferences(phrenPath);
248
+ const hooksEnabled = prefs.hooksEnabled !== false;
249
+ const toolPrefs = prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {};
250
+ const project = getOptionValue(args.slice(1), "--project");
251
+ if (project && (!isValidProjectName(project) || !fs.existsSync(path.join(phrenPath, project)))) {
252
+ console.error(`Project "${project}" not found.`);
253
+ process.exit(1);
254
+ }
255
+ const rows = HOOK_TOOLS.map((tool) => ({
256
+ tool,
257
+ hookType: "lifecycle",
258
+ status: hooksEnabled && toolPrefs[tool] !== false ? "enabled" : "disabled",
259
+ }));
260
+ console.log("Tool Hook Type Status");
261
+ console.log("-------- --------- --------");
262
+ for (const row of rows) {
263
+ console.log(`${row.tool.padEnd(8)} ${row.hookType.padEnd(9)} ${row.status}`);
264
+ }
265
+ if (project) {
266
+ const projectConfig = readProjectConfig(phrenPath, project);
267
+ const base = projectConfig.hooks?.enabled;
268
+ console.log("");
269
+ console.log(`Project ${project}`);
270
+ console.log(` base: ${typeof base === "boolean" ? (base ? "enabled" : "disabled") : "inherit"}`);
271
+ for (const event of PROJECT_HOOK_EVENTS) {
272
+ const configured = projectConfig.hooks?.[event];
273
+ const effective = isProjectHookEnabled(phrenPath, project, event, projectConfig);
274
+ console.log(` ${event}: ${effective ? "enabled" : "disabled"}${typeof configured === "boolean" ? ` (explicit ${configured ? "on" : "off"})` : " (inherit)"}`);
275
+ }
276
+ }
277
+ const customHooks = readCustomHooks(phrenPath);
278
+ if (customHooks.length > 0) {
279
+ console.log("");
280
+ console.log(`${customHooks.length} custom hook(s):`);
281
+ for (const h of customHooks) {
282
+ const hookKind = "webhook" in h ? "[webhook] " : "";
283
+ console.log(` ${h.event}: ${hookKind}${getHookTarget(h)}${h.timeout ? ` (${h.timeout}ms)` : ""}`);
284
+ }
285
+ }
286
+ return;
287
+ }
288
+ if (subcommand === "show" || subcommand === "edit") {
289
+ const tool = normalizeHookTool(args[1]);
290
+ if (!tool) {
291
+ printHooksUsage();
292
+ process.exit(1);
293
+ }
294
+ const configPath = hookConfigPath(tool, getPhrenPath());
295
+ if (!configPath || !fs.existsSync(configPath)) {
296
+ console.error(`Hook config not found for "${tool}": ${configPath ?? "(unknown path)"}`);
297
+ process.exit(1);
298
+ }
299
+ if (subcommand === "show") {
300
+ console.log(fs.readFileSync(configPath, "utf8"));
301
+ }
302
+ else {
303
+ openInEditor(configPath);
304
+ }
305
+ return;
306
+ }
307
+ if (subcommand === "enable" || subcommand === "disable") {
308
+ const tool = normalizeHookTool(args[1]);
309
+ if (!tool) {
310
+ printHooksUsage();
311
+ process.exit(1);
312
+ }
313
+ const prefs = readInstallPreferences(getPhrenPath());
314
+ writeInstallPreferences(getPhrenPath(), {
315
+ hookTools: {
316
+ ...(prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {}),
317
+ [tool]: subcommand === "enable",
318
+ },
319
+ });
320
+ console.log(`${subcommand === "enable" ? "Enabled" : "Disabled"} hooks for ${tool}.`);
321
+ return;
322
+ }
323
+ if (subcommand === "add-custom") {
324
+ const event = args[1];
325
+ const command = args.slice(2).join(" ");
326
+ if (!event || !command) {
327
+ console.error('Usage: phren hooks add-custom <event> "<command>"');
328
+ console.error("Events: " + HOOK_EVENT_VALUES.join(", "));
329
+ process.exit(1);
330
+ }
331
+ if (!HOOK_EVENT_VALUES.includes(event)) {
332
+ console.error(`Invalid event "${event}". Valid events: ${HOOK_EVENT_VALUES.join(", ")}`);
333
+ process.exit(1);
334
+ }
335
+ const phrenPath = getPhrenPath();
336
+ const prefs = readInstallPreferences(phrenPath);
337
+ const existing = Array.isArray(prefs.customHooks) ? prefs.customHooks : [];
338
+ const newHook = { event: event, command };
339
+ writeInstallPreferences(phrenPath, { ...prefs, customHooks: [...existing, newHook] });
340
+ console.log(`Added custom hook for "${event}": ${command}`);
341
+ return;
342
+ }
343
+ if (subcommand === "remove-custom") {
344
+ const event = args[1];
345
+ if (!event) {
346
+ console.error('Usage: phren hooks remove-custom <event> [<command>]');
347
+ process.exit(1);
348
+ }
349
+ if (!HOOK_EVENT_VALUES.includes(event)) {
350
+ console.error(`Invalid event "${event}". Valid events: ${HOOK_EVENT_VALUES.join(", ")}`);
351
+ process.exit(1);
352
+ }
353
+ const command = args.slice(2).join(" ") || undefined;
354
+ const phrenPath = getPhrenPath();
355
+ const prefs = readInstallPreferences(phrenPath);
356
+ const existing = Array.isArray(prefs.customHooks) ? prefs.customHooks : [];
357
+ const remaining = existing.filter(h => h.event !== event || (command && !getHookTarget(h).includes(command)));
358
+ const removed = existing.length - remaining.length;
359
+ if (removed === 0) {
360
+ console.error(`No custom hooks matched event="${event}"${command ? ` command containing "${command}"` : ""}.`);
361
+ process.exit(1);
362
+ }
363
+ writeInstallPreferences(phrenPath, { ...prefs, customHooks: remaining });
364
+ console.log(`Removed ${removed} custom hook(s) for "${event}".`);
365
+ return;
366
+ }
367
+ if (subcommand === "errors") {
368
+ const phrenPath = getPhrenPath();
369
+ const logPath = runtimeFile(phrenPath, "hook-errors.log");
370
+ if (!fs.existsSync(logPath)) {
371
+ console.log("No hook errors recorded.");
372
+ return;
373
+ }
374
+ const content = fs.readFileSync(logPath, "utf8").trim();
375
+ if (!content) {
376
+ console.log("No hook errors recorded.");
377
+ return;
378
+ }
379
+ const lines = content.split("\n");
380
+ const limitArg = getOptionValue(args.slice(1), "--limit");
381
+ const limit = limitArg ? Math.max(1, parseInt(limitArg, 10) || 20) : 20;
382
+ const display = lines.slice(-limit);
383
+ console.log(`Hook errors (last ${display.length} of ${lines.length}):\n`);
384
+ for (const line of display) {
385
+ console.log(line);
386
+ }
387
+ return;
388
+ }
389
+ console.error(`Unknown hooks subcommand: ${subcommand}`);
390
+ printHooksUsage();
391
+ process.exit(1);
392
+ }
393
+ export function handleSkillList(profile, project) {
394
+ if (project) {
395
+ const manifest = buildSkillManifest(getPhrenPath(), profile, project, resolveSkillMirrorDir(project) || undefined);
396
+ printResolvedManifest(project, manifest, resolveSkillMirrorDir(project));
397
+ return;
398
+ }
399
+ const sources = getAllSkills(getPhrenPath(), profile);
400
+ if (!sources.length) {
401
+ console.log("No skills found.");
402
+ return;
403
+ }
404
+ const nameWidth = Math.max(4, ...sources.map((source) => source.name.length));
405
+ const sourceWidth = Math.max(6, ...sources.map((source) => source.source.length));
406
+ const formatWidth = Math.max(6, ...sources.map((source) => source.format.length));
407
+ const commandWidth = Math.max(7, ...sources.map((source) => source.command.length));
408
+ const statusWidth = 8;
409
+ console.log(`${"Name".padEnd(nameWidth)} ${"Source".padEnd(sourceWidth)} ${"Format".padEnd(formatWidth)} ${"Command".padEnd(commandWidth)} ${"Status".padEnd(statusWidth)} Path`);
410
+ console.log(`${"─".repeat(nameWidth)} ${"─".repeat(sourceWidth)} ${"─".repeat(formatWidth)} ${"─".repeat(commandWidth)} ${"─".repeat(statusWidth)} ${"─".repeat(30)}`);
411
+ for (const skill of sources) {
412
+ console.log(`${skill.name.padEnd(nameWidth)} ${skill.source.padEnd(sourceWidth)} ${skill.format.padEnd(formatWidth)} ${skill.command.padEnd(commandWidth)} ${(skill.enabled ? "enabled" : "disabled").padEnd(statusWidth)} ${skill.path}`);
413
+ }
414
+ console.log(`\n${sources.length} skill(s) found.`);
415
+ }
416
+ export function handleDetectSkills(args, profile) {
417
+ const importFlag = args.includes("--import");
418
+ const nativeSkillsDir = homePath(".claude", "skills");
419
+ if (!fs.existsSync(nativeSkillsDir)) {
420
+ console.log("No native skills directory found at ~/.claude/skills/");
421
+ return;
422
+ }
423
+ const trackedSkills = new Set();
424
+ const phrenPath = getPhrenPath();
425
+ const globalSkillsDir = path.join(phrenPath, "global", "skills");
426
+ if (fs.existsSync(globalSkillsDir)) {
427
+ for (const entry of fs.readdirSync(globalSkillsDir)) {
428
+ trackedSkills.add(entry.replace(/\.md$/, ""));
429
+ if (fs.statSync(path.join(globalSkillsDir, entry)).isDirectory()) {
430
+ trackedSkills.add(entry);
431
+ }
432
+ }
433
+ }
434
+ for (const dir of getProjectDirs(phrenPath, profile)) {
435
+ for (const projectSkillsDir of [path.join(dir, "skills"), path.join(dir, ".claude", "skills")]) {
436
+ if (!fs.existsSync(projectSkillsDir))
437
+ continue;
438
+ for (const entry of fs.readdirSync(projectSkillsDir)) {
439
+ trackedSkills.add(entry.replace(/\.md$/, ""));
440
+ }
441
+ }
442
+ }
443
+ const untracked = [];
444
+ for (const entry of fs.readdirSync(nativeSkillsDir)) {
445
+ const entryPath = path.join(nativeSkillsDir, entry);
446
+ const stat = fs.statSync(entryPath);
447
+ try {
448
+ if (fs.lstatSync(entryPath).isSymbolicLink())
449
+ continue;
450
+ }
451
+ catch (err) {
452
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
453
+ process.stderr.write(`[phren] skillList lstat: ${errorMessage(err)}\n`);
454
+ }
455
+ const name = entry.replace(/\.md$/, "");
456
+ if (trackedSkills.has(name))
457
+ continue;
458
+ if (stat.isFile() && entry.endsWith(".md")) {
459
+ untracked.push({ name, path: entryPath, isDir: false });
460
+ }
461
+ else if (stat.isDirectory()) {
462
+ const skillFile = path.join(entryPath, "SKILL.md");
463
+ if (fs.existsSync(skillFile)) {
464
+ untracked.push({ name, path: entryPath, isDir: true });
465
+ }
466
+ }
467
+ }
468
+ if (!untracked.length) {
469
+ console.log("All skills in ~/.claude/skills/ are already tracked by phren.");
470
+ return;
471
+ }
472
+ console.log(`Found ${untracked.length} untracked skill(s) in ~/.claude/skills/:\n`);
473
+ for (const skill of untracked) {
474
+ console.log(` ${skill.name} (${skill.path})`);
475
+ }
476
+ if (!importFlag) {
477
+ console.log("\nRun with --import to copy these into phren global skills.");
478
+ return;
479
+ }
480
+ fs.mkdirSync(globalSkillsDir, { recursive: true });
481
+ let imported = 0;
482
+ for (const skill of untracked) {
483
+ const dest = skill.isDir
484
+ ? path.join(globalSkillsDir, skill.name)
485
+ : path.join(globalSkillsDir, `${skill.name}.md`);
486
+ if (fs.existsSync(dest)) {
487
+ console.log(` skip ${skill.name} (already exists in global/skills/)`);
488
+ continue;
489
+ }
490
+ if (skill.isDir) {
491
+ fs.cpSync(skill.path, dest, { recursive: true });
492
+ }
493
+ else {
494
+ fs.copyFileSync(skill.path, dest);
495
+ }
496
+ const destDisplay = skill.isDir ? `global/skills/${skill.name}/` : `global/skills/${skill.name}.md`;
497
+ console.log(` imported ${skill.name} -> ${destDisplay}`);
498
+ imported++;
499
+ }
500
+ console.log(`\nImported ${imported} skill(s). They are now tracked in phren global skills.`);
501
+ }
502
+ function resolveSkillMirrorDir(scope) {
503
+ if (scope.toLowerCase() === "global")
504
+ return homePath(".claude", "skills");
505
+ const projectDir = findProjectDir(scope);
506
+ return projectDir ? path.join(projectDir, ".claude", "skills") : null;
507
+ }
508
+ function printResolvedManifest(scope, manifest, destDir) {
509
+ console.log(`Scope: ${scope}`);
510
+ console.log(`Mirror: ${destDir || "(unavailable on disk)"}`);
511
+ console.log("");
512
+ for (const skill of manifest.skills) {
513
+ const status = skill.visibleToAgents ? "visible" : "disabled";
514
+ const overrideText = skill.overrides.length ? ` override:${skill.overrides.length}` : "";
515
+ console.log(`${skill.command} ${skill.name} ${skill.source} ${status}${overrideText}`);
516
+ console.log(` ${skill.path}`);
517
+ }
518
+ if (manifest.problems.length) {
519
+ console.log("\nProblems:");
520
+ for (const problem of manifest.problems) {
521
+ console.log(`- ${problem.message}`);
522
+ }
523
+ }
524
+ }
525
+ function printSkillDoctor(scope, manifest, destDir) {
526
+ printResolvedManifest(scope, manifest, destDir);
527
+ const problems = [];
528
+ if (!destDir) {
529
+ problems.push(`Mirror target for ${scope} is not discoverable on disk.`);
530
+ }
531
+ else {
532
+ const parentDir = path.dirname(destDir);
533
+ if (!fs.existsSync(path.join(parentDir, "skill-manifest.json"))) {
534
+ problems.push(`Missing generated manifest: ${path.join(parentDir, "skill-manifest.json")}`);
535
+ }
536
+ if (!fs.existsSync(path.join(parentDir, "skill-commands.json"))) {
537
+ problems.push(`Missing generated command registry: ${path.join(parentDir, "skill-commands.json")}`);
538
+ }
539
+ for (const skill of manifest.skills.filter((entry) => entry.visibleToAgents)) {
540
+ const dest = path.join(destDir, skill.format === "folder" ? skill.name : path.basename(skill.path));
541
+ try {
542
+ if (!fs.existsSync(dest) || fs.realpathSync(dest) !== fs.realpathSync(skill.root)) {
543
+ problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
544
+ }
545
+ }
546
+ catch {
547
+ problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
548
+ }
549
+ }
550
+ }
551
+ if (!manifest.problems.length && !problems.length) {
552
+ console.log("\nDoctor: no skill pipeline issues detected.");
553
+ return;
554
+ }
555
+ console.log("\nDoctor findings:");
556
+ for (const problem of [...manifest.problems.map((entry) => entry.message), ...problems]) {
557
+ console.log(`- ${problem}`);
558
+ }
559
+ }
560
+ export async function handleProjectsNamespace(args, profile) {
561
+ const subcommand = args[0];
562
+ if (!subcommand || subcommand === "list" || subcommand === "--help" || subcommand === "-h") {
563
+ if (subcommand === "--help" || subcommand === "-h") {
564
+ console.log("Usage:");
565
+ console.log(" phren projects list List all projects");
566
+ console.log(" phren projects configure <name> Update per-project enrollment settings");
567
+ console.log(" flags: --ownership=<mode> --hooks=on|off");
568
+ console.log(" phren projects remove <name> Remove a project (asks for confirmation)");
569
+ return;
570
+ }
571
+ return handleProjectsList(profile);
572
+ }
573
+ if (subcommand === "add") {
574
+ console.error("`phren projects add` has been removed from the supported workflow.");
575
+ console.error("Use `cd ~/your-project && npx phren add` so enrollment stays path-based.");
576
+ process.exit(1);
577
+ }
578
+ if (subcommand === "remove") {
579
+ const manifest = readRootManifest(getPhrenPath());
580
+ if (manifest?.installMode === "project-local") {
581
+ console.error("projects remove is unsupported in project-local mode. Use `phren uninstall`.");
582
+ process.exit(1);
583
+ }
584
+ const name = args[1];
585
+ if (!name) {
586
+ console.error("Usage: phren projects remove <name>");
587
+ process.exit(1);
588
+ }
589
+ return handleProjectsRemove(name, profile);
590
+ }
591
+ if (subcommand === "configure") {
592
+ const name = args[1];
593
+ if (!name) {
594
+ console.error(`Usage: phren projects configure <name> [--ownership=${PROJECT_OWNERSHIP_MODES.join("|")}] [--hooks=on|off]`);
595
+ process.exit(1);
596
+ }
597
+ if (!isValidProjectName(name)) {
598
+ console.error(`Invalid project name: "${name}".`);
599
+ process.exit(1);
600
+ }
601
+ if (!fs.existsSync(path.join(getPhrenPath(), name))) {
602
+ console.error(`Project "${name}" not found.`);
603
+ process.exit(1);
604
+ }
605
+ const ownershipArg = args.find((arg) => arg.startsWith("--ownership="))?.slice("--ownership=".length);
606
+ const hooksArg = args.find((arg) => arg.startsWith("--hooks="))?.slice("--hooks=".length);
607
+ const ownership = ownershipArg ? parseProjectOwnershipMode(ownershipArg) : undefined;
608
+ const hooksEnabled = parseMcpToggle(hooksArg);
609
+ if (!ownershipArg && hooksArg === undefined) {
610
+ console.error(`Usage: phren projects configure <name> [--ownership=${PROJECT_OWNERSHIP_MODES.join("|")}] [--hooks=on|off]`);
611
+ process.exit(1);
612
+ }
613
+ if (ownershipArg && !ownership) {
614
+ console.error(`Usage: phren projects configure <name> [--ownership=${PROJECT_OWNERSHIP_MODES.join("|")}] [--hooks=on|off]`);
615
+ process.exit(1);
616
+ }
617
+ if (hooksArg !== undefined && hooksEnabled === undefined) {
618
+ console.error(`Invalid --hooks value "${hooksArg}". Use on or off.`);
619
+ process.exit(1);
620
+ }
621
+ const updates = [];
622
+ if (ownership) {
623
+ writeProjectConfig(getPhrenPath(), name, { ownership });
624
+ updates.push(`ownership=${ownership}`);
625
+ }
626
+ if (hooksEnabled !== undefined) {
627
+ writeProjectHookConfig(getPhrenPath(), name, { enabled: hooksEnabled });
628
+ updates.push(`hooks=${hooksEnabled ? "on" : "off"}`);
629
+ }
630
+ console.log(`Updated ${name}: ${updates.join(", ")}`);
631
+ return;
632
+ }
633
+ console.error(`Unknown subcommand: ${subcommand}`);
634
+ console.error("Usage: phren projects [list|configure|remove]");
635
+ process.exit(1);
636
+ }
637
+ function handleProjectsList(profile) {
638
+ const phrenPath = getPhrenPath();
639
+ const projectDirs = getProjectDirs(phrenPath, profile);
640
+ const projects = projectDirs
641
+ .map((dir) => path.basename(dir))
642
+ .filter((name) => name !== "global")
643
+ .sort();
644
+ if (!projects.length) {
645
+ console.log("No projects found. Run: cd ~/your-project && npx phren add");
646
+ return;
647
+ }
648
+ console.log(`\nProjects in ${phrenPath}:\n`);
649
+ for (const name of projects) {
650
+ const projectDir = path.join(phrenPath, name);
651
+ let dirFiles;
652
+ try {
653
+ dirFiles = new Set(fs.readdirSync(projectDir));
654
+ }
655
+ catch (err) {
656
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
657
+ process.stderr.write(`[phren] projects list readdir: ${errorMessage(err)}\n`);
658
+ dirFiles = new Set();
659
+ }
660
+ const tags = [];
661
+ if (dirFiles.has("FINDINGS.md"))
662
+ tags.push("findings");
663
+ if (TASK_FILE_ALIASES.some((filename) => dirFiles.has(filename)))
664
+ tags.push("tasks");
665
+ const tagStr = tags.length ? ` [${tags.join(", ")}]` : "";
666
+ console.log(` ${name}${tagStr}`);
667
+ }
668
+ console.log(`\n${projects.length} project(s) total.`);
669
+ console.log("Add another project: cd ~/your-project && npx phren add");
670
+ }
671
+ async function handleProjectsRemove(name, profile) {
672
+ if (!isValidProjectName(name)) {
673
+ console.error(`Invalid project name: "${name}".`);
674
+ process.exit(1);
675
+ }
676
+ if (name === "global") {
677
+ console.error('Cannot remove the "global" project.');
678
+ process.exit(1);
679
+ }
680
+ const phrenPath = getPhrenPath();
681
+ const projectDir = path.join(phrenPath, name);
682
+ if (!fs.existsSync(projectDir)) {
683
+ console.error(`Project "${name}" not found at ${projectDir}`);
684
+ process.exit(1);
685
+ }
686
+ let fileCount = 0;
687
+ const countFiles = (dir) => {
688
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
689
+ if (entry.isDirectory())
690
+ countFiles(path.join(dir, entry.name));
691
+ else
692
+ fileCount++;
693
+ }
694
+ };
695
+ try {
696
+ countFiles(projectDir);
697
+ }
698
+ catch (err) {
699
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
700
+ process.stderr.write(`[phren] projects remove countFiles: ${errorMessage(err)}\n`);
701
+ }
702
+ const readline = await import("readline");
703
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
704
+ const answer = await new Promise((resolve) => {
705
+ rl.question(`Remove project "${name}" (${fileCount} file${fileCount === 1 ? "" : "s"})? This cannot be undone. Type the project name to confirm: `, (input) => { rl.close(); resolve(input.trim()); });
706
+ });
707
+ if (answer !== name) {
708
+ console.log("Aborted.");
709
+ return;
710
+ }
711
+ fs.rmSync(projectDir, { recursive: true, force: true });
712
+ console.log(`Removed project "${name}".`);
713
+ console.log(`If this project was in a profile, remove it from profiles/${profile || "personal"}.yaml manually.`);
714
+ }
715
+ // ── Task namespace ────────────────────────────────────────────────────────────
716
+ function printTaskUsage() {
717
+ console.log("Usage:");
718
+ console.log(' phren task add <project> "<text>"');
719
+ console.log(' phren task complete <project> "<text>"');
720
+ console.log(' phren task update <project> "<text>" [--priority=high|medium|low] [--section=Active|Queue|Done] [--context="..."]');
721
+ console.log(' phren task pin <project> "<text>"');
722
+ console.log(' phren task reorder <project> "<text>" --rank=<n>');
723
+ }
724
+ export async function handleTaskNamespace(args) {
725
+ const subcommand = args[0];
726
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
727
+ printTaskUsage();
728
+ return;
729
+ }
730
+ if (subcommand === "list") {
731
+ // Delegate to the cross-project task view (same as `phren tasks`)
732
+ const { handleTaskView } = await import("./cli-ops.js");
733
+ return handleTaskView(args[1] || "default");
734
+ }
735
+ if (subcommand === "add") {
736
+ const project = args[1];
737
+ const text = args.slice(2).join(" ");
738
+ if (!project || !text) {
739
+ console.error('Usage: phren task add <project> "<text>"');
740
+ process.exit(1);
741
+ }
742
+ const result = addTask(getPhrenPath(), project, text);
743
+ if (!result.ok) {
744
+ console.error(result.error);
745
+ process.exit(1);
746
+ }
747
+ console.log(`Task added: ${result.data.line}`);
748
+ return;
749
+ }
750
+ if (subcommand === "complete") {
751
+ const project = args[1];
752
+ const match = args.slice(2).join(" ");
753
+ if (!project || !match) {
754
+ console.error('Usage: phren task complete <project> "<text>"');
755
+ process.exit(1);
756
+ }
757
+ const result = completeTask(getPhrenPath(), project, match);
758
+ if (!result.ok) {
759
+ console.error(result.error);
760
+ process.exit(1);
761
+ }
762
+ console.log(result.data);
763
+ return;
764
+ }
765
+ if (subcommand === "update") {
766
+ const project = args[1];
767
+ if (!project) {
768
+ printTaskUsage();
769
+ process.exit(1);
770
+ }
771
+ // Collect non-flag args as the match text, flags as updates
772
+ const positional = [];
773
+ const updates = {};
774
+ for (const arg of args.slice(2)) {
775
+ if (arg.startsWith("--priority=")) {
776
+ updates.priority = arg.slice("--priority=".length);
777
+ }
778
+ else if (arg.startsWith("--section=")) {
779
+ updates.section = arg.slice("--section=".length);
780
+ }
781
+ else if (arg.startsWith("--context=")) {
782
+ updates.context = arg.slice("--context=".length);
783
+ }
784
+ else if (!arg.startsWith("--")) {
785
+ positional.push(arg);
786
+ }
787
+ }
788
+ const match = positional.join(" ");
789
+ if (!match) {
790
+ printTaskUsage();
791
+ process.exit(1);
792
+ }
793
+ const result = updateTask(getPhrenPath(), project, match, updates);
794
+ if (!result.ok) {
795
+ console.error(result.error);
796
+ process.exit(1);
797
+ }
798
+ console.log(result.data);
799
+ return;
800
+ }
801
+ if (subcommand === "pin") {
802
+ const project = args[1];
803
+ const match = args.slice(2).join(" ");
804
+ if (!project || !match) {
805
+ console.error('Usage: phren task pin <project> "<text>"');
806
+ process.exit(1);
807
+ }
808
+ const result = pinTask(getPhrenPath(), project, match);
809
+ if (!result.ok) {
810
+ console.error(result.error);
811
+ process.exit(1);
812
+ }
813
+ console.log(result.data);
814
+ return;
815
+ }
816
+ if (subcommand === "reorder") {
817
+ const project = args[1];
818
+ if (!project) {
819
+ printTaskUsage();
820
+ process.exit(1);
821
+ }
822
+ const positional = [];
823
+ let rankArg;
824
+ for (const arg of args.slice(2)) {
825
+ if (arg.startsWith("--rank=")) {
826
+ rankArg = arg.slice("--rank=".length);
827
+ }
828
+ else if (!arg.startsWith("--")) {
829
+ positional.push(arg);
830
+ }
831
+ }
832
+ const match = positional.join(" ");
833
+ const rank = rankArg ? Number.parseInt(rankArg, 10) : Number.NaN;
834
+ if (!match || !rankArg || !Number.isFinite(rank) || rank < 1) {
835
+ console.error('Usage: phren task reorder <project> "<text>" --rank=<n>');
836
+ process.exit(1);
837
+ }
838
+ const result = reorderTask(getPhrenPath(), project, match, rank);
839
+ if (!result.ok) {
840
+ console.error(result.error);
841
+ process.exit(1);
842
+ }
843
+ console.log(result.data);
844
+ return;
845
+ }
846
+ console.error(`Unknown task subcommand: ${subcommand}`);
847
+ printTaskUsage();
848
+ process.exit(1);
849
+ }
850
+ // ── Finding namespace ─────────────────────────────────────────────────────────
851
+ function printFindingUsage() {
852
+ console.log("Usage:");
853
+ console.log(' phren finding add <project> "<text>"');
854
+ console.log(' phren finding remove <project> "<text>"');
855
+ console.log(' phren finding supersede <project> "<text>" --by "<newer guidance>"');
856
+ console.log(' phren finding retract <project> "<text>" --reason "<reason>"');
857
+ }
858
+ export async function handleFindingNamespace(args) {
859
+ const subcommand = args[0];
860
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
861
+ printFindingUsage();
862
+ return;
863
+ }
864
+ if (subcommand === "list") {
865
+ const project = args[1];
866
+ if (!project) {
867
+ console.error("Usage: phren finding list <project>");
868
+ process.exit(1);
869
+ }
870
+ const { readFindings } = await import("./data-access.js");
871
+ const result = readFindings(getPhrenPath(), project);
872
+ if (!result.ok) {
873
+ console.error(result.error);
874
+ process.exit(1);
875
+ }
876
+ const items = result.data;
877
+ if (!items.length) {
878
+ console.log(`No findings found for "${project}".`);
879
+ return;
880
+ }
881
+ for (const entry of items.slice(0, 50)) {
882
+ console.log(`- [${entry.id}] ${entry.date}: ${entry.text}`);
883
+ }
884
+ return;
885
+ }
886
+ if (subcommand === "add") {
887
+ const project = args[1];
888
+ const text = args.slice(2).join(" ");
889
+ if (!project || !text) {
890
+ console.error('Usage: phren finding add <project> "<text>"');
891
+ process.exit(1);
892
+ }
893
+ const result = addFinding(getPhrenPath(), project, text);
894
+ if (!result.ok) {
895
+ console.error(result.message);
896
+ process.exit(1);
897
+ }
898
+ console.log(result.message);
899
+ return;
900
+ }
901
+ if (subcommand === "remove") {
902
+ const project = args[1];
903
+ const text = args.slice(2).join(" ");
904
+ if (!project || !text) {
905
+ console.error('Usage: phren finding remove <project> "<text>"');
906
+ process.exit(1);
907
+ }
908
+ const result = removeFinding(getPhrenPath(), project, text);
909
+ if (!result.ok) {
910
+ console.error(result.message);
911
+ process.exit(1);
912
+ }
913
+ console.log(result.message);
914
+ return;
915
+ }
916
+ if (subcommand === "supersede") {
917
+ const project = args[1];
918
+ if (!project) {
919
+ console.error('Usage: phren finding supersede <project> "<text>" --by "<newer guidance>"');
920
+ process.exit(1);
921
+ }
922
+ const rest = args.slice(2);
923
+ const byIdx = rest.indexOf("--by");
924
+ const byEqIdx = rest.findIndex(a => a.startsWith("--by="));
925
+ let text;
926
+ let byValue;
927
+ if (byEqIdx !== -1) {
928
+ byValue = rest[byEqIdx].slice("--by=".length);
929
+ text = rest.filter((_, i) => i !== byEqIdx && !rest[i].startsWith("--")).join(" ");
930
+ }
931
+ else if (byIdx !== -1) {
932
+ text = rest.slice(0, byIdx).join(" ");
933
+ byValue = rest.slice(byIdx + 1).join(" ");
934
+ }
935
+ else {
936
+ text = "";
937
+ byValue = "";
938
+ }
939
+ if (!text || !byValue) {
940
+ console.error('Usage: phren finding supersede <project> "<text>" --by "<newer guidance>"');
941
+ process.exit(1);
942
+ }
943
+ const result = supersedeFinding(getPhrenPath(), project, text, byValue);
944
+ if (!result.ok) {
945
+ console.error(result.error);
946
+ process.exit(1);
947
+ }
948
+ console.log(`Finding superseded: "${result.data.finding}" -> "${result.data.superseded_by}"`);
949
+ return;
950
+ }
951
+ if (subcommand === "retract") {
952
+ const project = args[1];
953
+ if (!project) {
954
+ console.error('Usage: phren finding retract <project> "<text>" --reason "<reason>"');
955
+ process.exit(1);
956
+ }
957
+ const rest = args.slice(2);
958
+ const reasonIdx = rest.indexOf("--reason");
959
+ const reasonEqIdx = rest.findIndex(a => a.startsWith("--reason="));
960
+ let text;
961
+ let reasonValue;
962
+ if (reasonEqIdx !== -1) {
963
+ reasonValue = rest[reasonEqIdx].slice("--reason=".length);
964
+ text = rest.filter((_, i) => i !== reasonEqIdx && !rest[i].startsWith("--")).join(" ");
965
+ }
966
+ else if (reasonIdx !== -1) {
967
+ text = rest.slice(0, reasonIdx).join(" ");
968
+ reasonValue = rest.slice(reasonIdx + 1).join(" ");
969
+ }
970
+ else {
971
+ text = "";
972
+ reasonValue = "";
973
+ }
974
+ if (!text || !reasonValue) {
975
+ console.error('Usage: phren finding retract <project> "<text>" --reason "<reason>"');
976
+ process.exit(1);
977
+ }
978
+ const result = retractFinding(getPhrenPath(), project, text, reasonValue);
979
+ if (!result.ok) {
980
+ console.error(result.error);
981
+ process.exit(1);
982
+ }
983
+ console.log(`Finding retracted: "${result.data.finding}" (reason: ${result.data.reason})`);
984
+ return;
985
+ }
986
+ console.error(`Unknown finding subcommand: ${subcommand}`);
987
+ printFindingUsage();
988
+ process.exit(1);
989
+ }