@phren/cli 0.1.13 → 0.1.14

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 (34) hide show
  1. package/dist/cli/hooks-session.d.ts +18 -36
  2. package/dist/cli/hooks-session.js +21 -1482
  3. package/dist/cli/namespaces-findings.d.ts +1 -0
  4. package/dist/cli/namespaces-findings.js +208 -0
  5. package/dist/cli/namespaces-profile.d.ts +1 -0
  6. package/dist/cli/namespaces-profile.js +76 -0
  7. package/dist/cli/namespaces-projects.d.ts +1 -0
  8. package/dist/cli/namespaces-projects.js +370 -0
  9. package/dist/cli/namespaces-review.d.ts +1 -0
  10. package/dist/cli/namespaces-review.js +45 -0
  11. package/dist/cli/namespaces-skills.d.ts +4 -0
  12. package/dist/cli/namespaces-skills.js +550 -0
  13. package/dist/cli/namespaces-store.d.ts +2 -0
  14. package/dist/cli/namespaces-store.js +367 -0
  15. package/dist/cli/namespaces-tasks.d.ts +1 -0
  16. package/dist/cli/namespaces-tasks.js +369 -0
  17. package/dist/cli/namespaces-utils.d.ts +4 -0
  18. package/dist/cli/namespaces-utils.js +47 -0
  19. package/dist/cli/namespaces.d.ts +7 -11
  20. package/dist/cli/namespaces.js +8 -2011
  21. package/dist/cli/session-background.d.ts +3 -0
  22. package/dist/cli/session-background.js +176 -0
  23. package/dist/cli/session-git.d.ts +17 -0
  24. package/dist/cli/session-git.js +181 -0
  25. package/dist/cli/session-metrics.d.ts +2 -0
  26. package/dist/cli/session-metrics.js +67 -0
  27. package/dist/cli/session-start.d.ts +3 -0
  28. package/dist/cli/session-start.js +289 -0
  29. package/dist/cli/session-stop.d.ts +8 -0
  30. package/dist/cli/session-stop.js +468 -0
  31. package/dist/cli/session-tool-hook.d.ts +18 -0
  32. package/dist/cli/session-tool-hook.js +376 -0
  33. package/dist/tools/search.js +1 -1
  34. package/package.json +1 -1
@@ -0,0 +1,45 @@
1
+ import { isValidProjectName } from "../utils.js";
2
+ import { getPhrenPath } from "../shared.js";
3
+ export async function handleReviewNamespace(args) {
4
+ const subcommand = args[0];
5
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
6
+ console.log("Usage:");
7
+ console.log(" phren review [project] Show review queue items");
8
+ console.log(" phren review approve <project> <text> Approve and remove item");
9
+ console.log(" phren review reject <project> <text> Reject and remove item");
10
+ console.log("");
11
+ console.log("Examples:");
12
+ console.log(' phren review myproject');
13
+ console.log(' phren review approve myproject "Always validate input"');
14
+ console.log(' phren review reject myproject "Avoid async in loops"');
15
+ return;
16
+ }
17
+ // Handle "approve" and "reject" subcommands
18
+ if (subcommand === "approve" || subcommand === "reject") {
19
+ const action = subcommand;
20
+ const project = args[1];
21
+ const lineText = args.slice(2).join(" ");
22
+ if (!project || !lineText) {
23
+ console.error(`Usage: phren review ${action} <project> <text>`);
24
+ process.exit(1);
25
+ }
26
+ if (!isValidProjectName(project)) {
27
+ console.error(`Invalid project name: "${project}".`);
28
+ process.exit(1);
29
+ }
30
+ const phrenPath = getPhrenPath();
31
+ const { approveQueueItem, rejectQueueItem } = await import("../data/access.js");
32
+ const result = action === "approve"
33
+ ? approveQueueItem(phrenPath, project, lineText)
34
+ : rejectQueueItem(phrenPath, project, lineText);
35
+ if (!result.ok) {
36
+ console.error(`Failed to ${action} item: ${result.error ?? "Unknown error"}`);
37
+ process.exit(1);
38
+ }
39
+ console.log(`${action === "approve" ? "✓ Approved" : "✗ Rejected"}: ${lineText.slice(0, 100)}${lineText.length > 100 ? "..." : ""}`);
40
+ return;
41
+ }
42
+ // Default: show review queue (first arg is project name if not a subcommand)
43
+ const { handleReview } = await import("./actions.js");
44
+ return handleReview(args);
45
+ }
@@ -0,0 +1,4 @@
1
+ export declare function handleSkillsNamespace(args: string[], profile: string): void;
2
+ export declare function handleHooksNamespace(args: string[]): void;
3
+ export declare function handleSkillList(profile: string, project?: string): void;
4
+ export declare function handleDetectSkills(args: string[], profile: string): void;
@@ -0,0 +1,550 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { expandHomePath, getPhrenPath, getProjectDirs, homePath, hookConfigPath, } from "../shared.js";
4
+ import { isValidProjectName, errorMessage } from "../utils.js";
5
+ import { logger } from "../logger.js";
6
+ import { readInstallPreferences, writeInstallPreferences } from "../init/preferences.js";
7
+ import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "../skill/registry.js";
8
+ import { detectSkillCollisions } from "../link/skills.js";
9
+ import { setSkillEnabledAndSync, syncSkillLinksForScope } from "../skill/files.js";
10
+ import { findProjectDir } from "../project-locator.js";
11
+ import { readCustomHooks, getHookTarget, HOOK_EVENT_VALUES, validateCustomHookCommand } from "../hooks.js";
12
+ import { runtimeFile } from "../shared.js";
13
+ import { readProjectConfig, PROJECT_HOOK_EVENTS, isProjectHookEnabled, } from "../project-config.js";
14
+ import { resolveProjectStorePath, getOptionValue, openInEditor } from "./namespaces-utils.js";
15
+ const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
16
+ function printSkillsUsage() {
17
+ console.log("Usage:");
18
+ console.log(" phren skills list [--project <name>]");
19
+ console.log(" phren skills show <name> [--project <name>]");
20
+ console.log(" phren skills edit <name> [--project <name>]");
21
+ console.log(" phren skills add <project> <path>");
22
+ console.log(" phren skills resolve <project|global> [--json]");
23
+ console.log(" phren skills doctor <project|global>");
24
+ console.log(" phren skills sync <project|global>");
25
+ console.log(" phren skills enable <project|global> <name>");
26
+ console.log(" phren skills disable <project|global> <name>");
27
+ console.log(" phren skills remove <project> <name>");
28
+ }
29
+ function printHooksUsage() {
30
+ console.log("Usage:");
31
+ console.log(" phren hooks list [--project <name>]");
32
+ console.log(" phren hooks show <tool>");
33
+ console.log(" phren hooks edit <tool>");
34
+ console.log(" phren hooks enable <tool>");
35
+ console.log(" phren hooks disable <tool>");
36
+ console.log(" phren hooks add-custom <event> <command>");
37
+ console.log(" phren hooks remove-custom <event> [<command>]");
38
+ console.log(" phren hooks errors [--limit <n>]");
39
+ console.log(" tools: claude|copilot|cursor|codex");
40
+ console.log(" events: " + HOOK_EVENT_VALUES.join(", "));
41
+ }
42
+ function normalizeHookTool(raw) {
43
+ if (!raw)
44
+ return null;
45
+ const tool = raw.toLowerCase();
46
+ return HOOK_TOOLS.includes(tool) ? tool : null;
47
+ }
48
+ function findSkillPath(name, profile, project) {
49
+ const found = findSkill(getPhrenPath(), profile, project, name);
50
+ if (!found || "error" in found)
51
+ return null;
52
+ return found.path;
53
+ }
54
+ function resolveSkillMirrorDir(scope) {
55
+ if (scope.toLowerCase() === "global")
56
+ return homePath(".claude", "skills");
57
+ const projectDir = findProjectDir(scope);
58
+ return projectDir ? path.join(projectDir, ".claude", "skills") : null;
59
+ }
60
+ function printResolvedManifest(scope, manifest, destDir) {
61
+ console.log(`Scope: ${scope}`);
62
+ console.log(`Mirror: ${destDir || "(unavailable on disk)"}`);
63
+ console.log("");
64
+ for (const skill of manifest.skills) {
65
+ const status = skill.visibleToAgents ? "visible" : "disabled";
66
+ const overrideText = skill.overrides.length ? ` override:${skill.overrides.length}` : "";
67
+ console.log(`${skill.command} ${skill.name} ${skill.source} ${status}${overrideText}`);
68
+ console.log(` ${skill.path}`);
69
+ }
70
+ if (manifest.problems.length) {
71
+ console.log("\nProblems:");
72
+ for (const problem of manifest.problems) {
73
+ console.log(`- ${problem.message}`);
74
+ }
75
+ }
76
+ }
77
+ function printSkillDoctor(scope, manifest, destDir) {
78
+ printResolvedManifest(scope, manifest, destDir);
79
+ const problems = [];
80
+ if (!destDir) {
81
+ problems.push(`Mirror target for ${scope} is not discoverable on disk.`);
82
+ }
83
+ else {
84
+ const parentDir = path.dirname(destDir);
85
+ if (!fs.existsSync(path.join(parentDir, "skill-manifest.json"))) {
86
+ problems.push(`Missing generated manifest: ${path.join(parentDir, "skill-manifest.json")}`);
87
+ }
88
+ if (!fs.existsSync(path.join(parentDir, "skill-commands.json"))) {
89
+ problems.push(`Missing generated command registry: ${path.join(parentDir, "skill-commands.json")}`);
90
+ }
91
+ for (const skill of manifest.skills.filter((entry) => entry.visibleToAgents)) {
92
+ const dest = path.join(destDir, skill.format === "folder" ? skill.name : path.basename(skill.path));
93
+ try {
94
+ if (!fs.existsSync(dest) || fs.realpathSync(dest) !== fs.realpathSync(skill.root)) {
95
+ problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
96
+ }
97
+ }
98
+ catch {
99
+ problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
100
+ }
101
+ }
102
+ // Check for user-owned files blocking phren skill links
103
+ const phrenPath = getPhrenPath();
104
+ const srcDir = scope.toLowerCase() === "global"
105
+ ? path.join(phrenPath, "global", "skills")
106
+ : path.join(phrenPath, scope, "skills");
107
+ const collisions = detectSkillCollisions(srcDir, destDir, phrenPath);
108
+ for (const collision of collisions) {
109
+ problems.push(`Skill collision: ${collision.message}`);
110
+ }
111
+ }
112
+ if (!manifest.problems.length && !problems.length) {
113
+ console.log("\nDoctor: no skill pipeline issues detected.");
114
+ return;
115
+ }
116
+ console.log("\nDoctor findings:");
117
+ for (const problem of [...manifest.problems.map((entry) => entry.message), ...problems]) {
118
+ console.log(`- ${problem}`);
119
+ }
120
+ }
121
+ export function handleSkillsNamespace(args, profile) {
122
+ const subcommand = args[0];
123
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
124
+ printSkillsUsage();
125
+ return;
126
+ }
127
+ if (subcommand === "list") {
128
+ const projectIdx = args.indexOf("--project");
129
+ const project = projectIdx !== -1 ? args[projectIdx + 1] : undefined;
130
+ handleSkillList(profile, project);
131
+ return;
132
+ }
133
+ if (subcommand === "show" || subcommand === "edit") {
134
+ const name = args[1];
135
+ if (!name) {
136
+ printSkillsUsage();
137
+ process.exit(1);
138
+ }
139
+ const projectIdx = args.indexOf("--project");
140
+ const project = projectIdx !== -1 ? args[projectIdx + 1] : undefined;
141
+ const skillPath = findSkillPath(name, profile, project);
142
+ if (!skillPath) {
143
+ console.error(`Skill not found: "${name}"${project ? ` in project "${project}"` : ""}`);
144
+ process.exit(1);
145
+ }
146
+ if (subcommand === "show") {
147
+ console.log(fs.readFileSync(skillPath, "utf8"));
148
+ }
149
+ else {
150
+ openInEditor(skillPath);
151
+ }
152
+ return;
153
+ }
154
+ if (subcommand === "add") {
155
+ const project = args[1];
156
+ const skillPath = args[2];
157
+ if (!project || !skillPath) {
158
+ printSkillsUsage();
159
+ process.exit(1);
160
+ }
161
+ if (!isValidProjectName(project)) {
162
+ console.error(`Invalid project name: "${project}"`);
163
+ process.exit(1);
164
+ }
165
+ const source = path.resolve(expandHomePath(skillPath));
166
+ if (!fs.existsSync(source) || !fs.statSync(source).isFile()) {
167
+ console.error(`Skill file not found: ${source}`);
168
+ process.exit(1);
169
+ }
170
+ const baseName = path.basename(source);
171
+ const fileName = baseName.toLowerCase().endsWith(".md") ? baseName : `${baseName}.md`;
172
+ const destDir = path.join(getPhrenPath(), project, "skills");
173
+ const dest = path.join(destDir, fileName);
174
+ fs.mkdirSync(destDir, { recursive: true });
175
+ if (fs.existsSync(dest)) {
176
+ console.error(`Skill already exists: ${dest}`);
177
+ process.exit(1);
178
+ }
179
+ try {
180
+ fs.symlinkSync(source, dest);
181
+ console.log(`Linked skill ${fileName} into ${project}.`);
182
+ }
183
+ catch (err) {
184
+ if ((process.env.PHREN_DEBUG))
185
+ logger.debug("cli-namespaces", `skill add symlinkFailed: ${errorMessage(err)}`);
186
+ fs.copyFileSync(source, dest);
187
+ console.log(`Copied skill ${fileName} into ${project}.`);
188
+ }
189
+ return;
190
+ }
191
+ if (subcommand === "resolve" || subcommand === "doctor" || subcommand === "sync") {
192
+ const scope = args[1];
193
+ if (!scope) {
194
+ printSkillsUsage();
195
+ process.exit(1);
196
+ }
197
+ if (scope.toLowerCase() !== "global" && !isValidProjectName(scope)) {
198
+ console.error(`Invalid project name: "${scope}"`);
199
+ process.exit(1);
200
+ }
201
+ if (subcommand === "sync") {
202
+ const syncedManifest = syncSkillLinksForScope(getPhrenPath(), scope);
203
+ if (!syncedManifest) {
204
+ console.error(`Project directory not found for "${scope}".`);
205
+ process.exit(1);
206
+ }
207
+ const mirrorDir = resolveSkillMirrorDir(scope) || homePath(".claude", "skills");
208
+ console.log(`Synced ${syncedManifest.skills.filter((skill) => skill.visibleToAgents).length} skill(s) for ${scope}.`);
209
+ console.log(` ${path.join(path.dirname(mirrorDir), "skill-manifest.json")}`);
210
+ console.log(` ${path.join(path.dirname(mirrorDir), "skill-commands.json")}`);
211
+ return;
212
+ }
213
+ const destDir = resolveSkillMirrorDir(scope);
214
+ const manifest = buildSkillManifest(getPhrenPath(), profile, scope, destDir || undefined);
215
+ if (subcommand === "resolve") {
216
+ if (args.includes("--json")) {
217
+ console.log(JSON.stringify(manifest, null, 2));
218
+ return;
219
+ }
220
+ printResolvedManifest(scope, manifest, destDir);
221
+ return;
222
+ }
223
+ printSkillDoctor(scope, manifest, destDir);
224
+ return;
225
+ }
226
+ if (subcommand === "enable" || subcommand === "disable") {
227
+ const scope = args[1];
228
+ const name = args[2];
229
+ if (!scope || !name) {
230
+ printSkillsUsage();
231
+ process.exit(1);
232
+ }
233
+ if (scope.toLowerCase() !== "global" && !isValidProjectName(scope)) {
234
+ console.error(`Invalid project name: "${scope}"`);
235
+ process.exit(1);
236
+ }
237
+ const resolved = findSkill(getPhrenPath(), profile, scope, name);
238
+ if (!resolved || "error" in resolved) {
239
+ console.error(`Skill not found: "${name}" in "${scope}"`);
240
+ process.exit(1);
241
+ }
242
+ setSkillEnabledAndSync(getPhrenPath(), scope, resolved.name, subcommand === "enable");
243
+ console.log(`${subcommand === "enable" ? "Enabled" : "Disabled"} skill ${resolved.name} in ${scope}.`);
244
+ return;
245
+ }
246
+ if (subcommand === "remove") {
247
+ const project = args[1];
248
+ const name = args[2];
249
+ if (!project || !name) {
250
+ printSkillsUsage();
251
+ process.exit(1);
252
+ }
253
+ if (!isValidProjectName(project)) {
254
+ console.error(`Invalid project name: "${project}"`);
255
+ process.exit(1);
256
+ }
257
+ const resolved = findLocalSkill(getPhrenPath(), project, name)?.path || null;
258
+ if (!resolved) {
259
+ console.error(`Skill not found: "${name}" in project "${project}"`);
260
+ process.exit(1);
261
+ }
262
+ const removePath = path.basename(resolved) === "SKILL.md" ? path.dirname(resolved) : resolved;
263
+ if (fs.statSync(removePath).isDirectory()) {
264
+ fs.rmSync(removePath, { recursive: true, force: true });
265
+ }
266
+ else {
267
+ fs.unlinkSync(removePath);
268
+ }
269
+ console.log(`Removed skill ${name.replace(/\.md$/i, "")} from ${project}.`);
270
+ return;
271
+ }
272
+ console.error(`Unknown skills subcommand: ${subcommand}`);
273
+ printSkillsUsage();
274
+ process.exit(1);
275
+ }
276
+ export function handleHooksNamespace(args) {
277
+ const subcommand = args[0];
278
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
279
+ printHooksUsage();
280
+ return;
281
+ }
282
+ if (subcommand === "list") {
283
+ const phrenPath = getPhrenPath();
284
+ const prefs = readInstallPreferences(phrenPath);
285
+ const hooksEnabled = prefs.hooksEnabled !== false;
286
+ const toolPrefs = prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {};
287
+ const project = getOptionValue(args.slice(1), "--project");
288
+ if (project && !isValidProjectName(project)) {
289
+ console.error(`Project "${project}" not found.`);
290
+ process.exit(1);
291
+ }
292
+ if (project) {
293
+ const storePath = resolveProjectStorePath(phrenPath, project);
294
+ if (!fs.existsSync(path.join(storePath, project))) {
295
+ console.error(`Project "${project}" not found.`);
296
+ process.exit(1);
297
+ }
298
+ }
299
+ const rows = HOOK_TOOLS.map((tool) => ({
300
+ tool,
301
+ hookType: "lifecycle",
302
+ status: hooksEnabled && toolPrefs[tool] !== false ? "enabled" : "disabled",
303
+ }));
304
+ console.log("Tool Hook Type Status");
305
+ console.log("-------- --------- --------");
306
+ for (const row of rows) {
307
+ console.log(`${row.tool.padEnd(8)} ${row.hookType.padEnd(9)} ${row.status}`);
308
+ }
309
+ if (project) {
310
+ const projectConfig = readProjectConfig(phrenPath, project);
311
+ const base = projectConfig.hooks?.enabled;
312
+ console.log("");
313
+ console.log(`Project ${project}`);
314
+ console.log(` base: ${typeof base === "boolean" ? (base ? "enabled" : "disabled") : "inherit"}`);
315
+ for (const event of PROJECT_HOOK_EVENTS) {
316
+ const configured = projectConfig.hooks?.[event];
317
+ const effective = isProjectHookEnabled(phrenPath, project, event, projectConfig);
318
+ console.log(` ${event}: ${effective ? "enabled" : "disabled"}${typeof configured === "boolean" ? ` (explicit ${configured ? "on" : "off"})` : " (inherit)"}`);
319
+ }
320
+ }
321
+ const customHooks = readCustomHooks(phrenPath);
322
+ if (customHooks.length > 0) {
323
+ console.log("");
324
+ console.log(`${customHooks.length} custom hook(s):`);
325
+ for (const h of customHooks) {
326
+ const hookKind = "webhook" in h ? "[webhook] " : "";
327
+ console.log(` ${h.event}: ${hookKind}${getHookTarget(h)}${h.timeout ? ` (${h.timeout}ms)` : ""}`);
328
+ }
329
+ }
330
+ return;
331
+ }
332
+ if (subcommand === "show" || subcommand === "edit") {
333
+ const tool = normalizeHookTool(args[1]);
334
+ if (!tool) {
335
+ printHooksUsage();
336
+ process.exit(1);
337
+ }
338
+ const configPath = hookConfigPath(tool, getPhrenPath());
339
+ if (!configPath || !fs.existsSync(configPath)) {
340
+ console.error(`Hook config not found for "${tool}": ${configPath ?? "(unknown path)"}`);
341
+ process.exit(1);
342
+ }
343
+ if (subcommand === "show") {
344
+ console.log(fs.readFileSync(configPath, "utf8"));
345
+ }
346
+ else {
347
+ openInEditor(configPath);
348
+ }
349
+ return;
350
+ }
351
+ if (subcommand === "enable" || subcommand === "disable") {
352
+ const tool = normalizeHookTool(args[1]);
353
+ if (!tool) {
354
+ printHooksUsage();
355
+ process.exit(1);
356
+ }
357
+ const prefs = readInstallPreferences(getPhrenPath());
358
+ writeInstallPreferences(getPhrenPath(), {
359
+ hookTools: {
360
+ ...(prefs.hookTools && typeof prefs.hookTools === "object" ? prefs.hookTools : {}),
361
+ [tool]: subcommand === "enable",
362
+ },
363
+ });
364
+ console.log(`${subcommand === "enable" ? "Enabled" : "Disabled"} hooks for ${tool}.`);
365
+ return;
366
+ }
367
+ if (subcommand === "add-custom") {
368
+ const event = args[1];
369
+ const command = args.slice(2).join(" ");
370
+ if (!event || !command) {
371
+ console.error('Usage: phren hooks add-custom <event> "<command>"');
372
+ console.error("Events: " + HOOK_EVENT_VALUES.join(", "));
373
+ process.exit(1);
374
+ }
375
+ if (!HOOK_EVENT_VALUES.includes(event)) {
376
+ console.error(`Invalid event "${event}". Valid events: ${HOOK_EVENT_VALUES.join(", ")}`);
377
+ process.exit(1);
378
+ }
379
+ const commandErr = validateCustomHookCommand(command);
380
+ if (commandErr) {
381
+ console.error(commandErr);
382
+ process.exit(1);
383
+ }
384
+ const phrenPath = getPhrenPath();
385
+ const prefs = readInstallPreferences(phrenPath);
386
+ const existing = Array.isArray(prefs.customHooks) ? prefs.customHooks : [];
387
+ const newHook = { event: event, command };
388
+ writeInstallPreferences(phrenPath, { ...prefs, customHooks: [...existing, newHook] });
389
+ console.log(`Added custom hook for "${event}": ${command}`);
390
+ return;
391
+ }
392
+ if (subcommand === "remove-custom") {
393
+ const event = args[1];
394
+ if (!event) {
395
+ console.error('Usage: phren hooks remove-custom <event> [<command>]');
396
+ process.exit(1);
397
+ }
398
+ if (!HOOK_EVENT_VALUES.includes(event)) {
399
+ console.error(`Invalid event "${event}". Valid events: ${HOOK_EVENT_VALUES.join(", ")}`);
400
+ process.exit(1);
401
+ }
402
+ const command = args.slice(2).join(" ") || undefined;
403
+ const phrenPath = getPhrenPath();
404
+ const prefs = readInstallPreferences(phrenPath);
405
+ const existing = Array.isArray(prefs.customHooks) ? prefs.customHooks : [];
406
+ const remaining = existing.filter(h => h.event !== event || (command && !getHookTarget(h).includes(command)));
407
+ const removed = existing.length - remaining.length;
408
+ if (removed === 0) {
409
+ console.error(`No custom hooks matched event="${event}"${command ? ` command containing "${command}"` : ""}.`);
410
+ process.exit(1);
411
+ }
412
+ writeInstallPreferences(phrenPath, { ...prefs, customHooks: remaining });
413
+ console.log(`Removed ${removed} custom hook(s) for "${event}".`);
414
+ return;
415
+ }
416
+ if (subcommand === "errors") {
417
+ const phrenPath = getPhrenPath();
418
+ const logPath = runtimeFile(phrenPath, "hook-errors.log");
419
+ if (!fs.existsSync(logPath)) {
420
+ console.log("No hook errors recorded.");
421
+ return;
422
+ }
423
+ const content = fs.readFileSync(logPath, "utf8").trim();
424
+ if (!content) {
425
+ console.log("No hook errors recorded.");
426
+ return;
427
+ }
428
+ const lines = content.split("\n");
429
+ const limitArg = getOptionValue(args.slice(1), "--limit");
430
+ const limit = limitArg ? Math.max(1, parseInt(limitArg, 10) || 20) : 20;
431
+ const display = lines.slice(-limit);
432
+ console.log(`Hook errors (last ${display.length} of ${lines.length}):\n`);
433
+ for (const line of display) {
434
+ console.log(line);
435
+ }
436
+ return;
437
+ }
438
+ console.error(`Unknown hooks subcommand: ${subcommand}`);
439
+ printHooksUsage();
440
+ process.exit(1);
441
+ }
442
+ export function handleSkillList(profile, project) {
443
+ if (project) {
444
+ const manifest = buildSkillManifest(getPhrenPath(), profile, project, resolveSkillMirrorDir(project) || undefined);
445
+ printResolvedManifest(project, manifest, resolveSkillMirrorDir(project));
446
+ return;
447
+ }
448
+ const sources = getAllSkills(getPhrenPath(), profile);
449
+ if (!sources.length) {
450
+ console.log("No skills found.");
451
+ return;
452
+ }
453
+ const nameWidth = Math.max(4, ...sources.map((source) => source.name.length));
454
+ const sourceWidth = Math.max(6, ...sources.map((source) => source.source.length));
455
+ const formatWidth = Math.max(6, ...sources.map((source) => source.format.length));
456
+ const commandWidth = Math.max(7, ...sources.map((source) => source.command.length));
457
+ const statusWidth = 8;
458
+ console.log(`${"Name".padEnd(nameWidth)} ${"Source".padEnd(sourceWidth)} ${"Format".padEnd(formatWidth)} ${"Command".padEnd(commandWidth)} ${"Status".padEnd(statusWidth)} Path`);
459
+ console.log(`${"─".repeat(nameWidth)} ${"─".repeat(sourceWidth)} ${"─".repeat(formatWidth)} ${"─".repeat(commandWidth)} ${"─".repeat(statusWidth)} ${"─".repeat(30)}`);
460
+ for (const skill of sources) {
461
+ 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}`);
462
+ }
463
+ console.log(`\n${sources.length} skill(s) found.`);
464
+ }
465
+ export function handleDetectSkills(args, profile) {
466
+ const importFlag = args.includes("--import");
467
+ const nativeSkillsDir = homePath(".claude", "skills");
468
+ if (!fs.existsSync(nativeSkillsDir)) {
469
+ console.log("No native skills directory found at ~/.claude/skills/");
470
+ return;
471
+ }
472
+ const trackedSkills = new Set();
473
+ const phrenPath = getPhrenPath();
474
+ const globalSkillsDir = path.join(phrenPath, "global", "skills");
475
+ if (fs.existsSync(globalSkillsDir)) {
476
+ for (const entry of fs.readdirSync(globalSkillsDir)) {
477
+ trackedSkills.add(entry.replace(/\.md$/, ""));
478
+ if (fs.statSync(path.join(globalSkillsDir, entry)).isDirectory()) {
479
+ trackedSkills.add(entry);
480
+ }
481
+ }
482
+ }
483
+ for (const dir of getProjectDirs(phrenPath, profile)) {
484
+ for (const projectSkillsDir of [path.join(dir, "skills"), path.join(dir, ".claude", "skills")]) {
485
+ if (!fs.existsSync(projectSkillsDir))
486
+ continue;
487
+ for (const entry of fs.readdirSync(projectSkillsDir)) {
488
+ trackedSkills.add(entry.replace(/\.md$/, ""));
489
+ }
490
+ }
491
+ }
492
+ const untracked = [];
493
+ for (const entry of fs.readdirSync(nativeSkillsDir)) {
494
+ const entryPath = path.join(nativeSkillsDir, entry);
495
+ const stat = fs.statSync(entryPath);
496
+ try {
497
+ if (fs.lstatSync(entryPath).isSymbolicLink())
498
+ continue;
499
+ }
500
+ catch (err) {
501
+ if ((process.env.PHREN_DEBUG))
502
+ logger.debug("cli-namespaces", `skillList lstat: ${errorMessage(err)}`);
503
+ }
504
+ const name = entry.replace(/\.md$/, "");
505
+ if (trackedSkills.has(name))
506
+ continue;
507
+ if (stat.isFile() && entry.endsWith(".md")) {
508
+ untracked.push({ name, path: entryPath, isDir: false });
509
+ }
510
+ else if (stat.isDirectory()) {
511
+ const skillFile = path.join(entryPath, "SKILL.md");
512
+ if (fs.existsSync(skillFile)) {
513
+ untracked.push({ name, path: entryPath, isDir: true });
514
+ }
515
+ }
516
+ }
517
+ if (!untracked.length) {
518
+ console.log("All skills in ~/.claude/skills/ are already tracked by phren.");
519
+ return;
520
+ }
521
+ console.log(`Found ${untracked.length} untracked skill(s) in ~/.claude/skills/:\n`);
522
+ for (const skill of untracked) {
523
+ console.log(` ${skill.name} (${skill.path})`);
524
+ }
525
+ if (!importFlag) {
526
+ console.log("\nRun with --import to copy these into phren global skills.");
527
+ return;
528
+ }
529
+ fs.mkdirSync(globalSkillsDir, { recursive: true });
530
+ let imported = 0;
531
+ for (const skill of untracked) {
532
+ const dest = skill.isDir
533
+ ? path.join(globalSkillsDir, skill.name)
534
+ : path.join(globalSkillsDir, `${skill.name}.md`);
535
+ if (fs.existsSync(dest)) {
536
+ console.log(` skip ${skill.name} (already exists in global/skills/)`);
537
+ continue;
538
+ }
539
+ if (skill.isDir) {
540
+ fs.cpSync(skill.path, dest, { recursive: true });
541
+ }
542
+ else {
543
+ fs.copyFileSync(skill.path, dest);
544
+ }
545
+ const destDisplay = skill.isDir ? `global/skills/${skill.name}/` : `global/skills/${skill.name}.md`;
546
+ console.log(` imported ${skill.name} -> ${destDisplay}`);
547
+ imported++;
548
+ }
549
+ console.log(`\nImported ${imported} skill(s). They are now tracked in phren global skills.`);
550
+ }
@@ -0,0 +1,2 @@
1
+ export declare function handleStoreNamespace(args: string[]): Promise<void>;
2
+ export declare function handlePromoteNamespace(args: string[]): Promise<void>;