@sporesec/arcana 2.3.1 → 3.0.0

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 (241) hide show
  1. package/dist/cli.d.ts +0 -1
  2. package/dist/cli.js +140 -10
  3. package/dist/command-registry.d.ts +10 -0
  4. package/dist/command-registry.js +65 -0
  5. package/dist/commands/audit.d.ts +0 -1
  6. package/dist/commands/audit.js +16 -6
  7. package/dist/commands/benchmark.d.ts +4 -0
  8. package/dist/commands/benchmark.js +178 -0
  9. package/dist/commands/clean.d.ts +2 -1
  10. package/dist/commands/clean.js +198 -47
  11. package/dist/commands/compact.d.ts +6 -0
  12. package/dist/commands/compact.js +239 -0
  13. package/dist/commands/completions.d.ts +3 -0
  14. package/dist/commands/completions.js +104 -0
  15. package/dist/commands/config.d.ts +0 -1
  16. package/dist/commands/config.js +15 -6
  17. package/dist/commands/create.d.ts +0 -1
  18. package/dist/commands/create.js +1 -1
  19. package/dist/commands/diff.d.ts +4 -0
  20. package/dist/commands/diff.js +166 -0
  21. package/dist/commands/doctor.d.ts +0 -1
  22. package/dist/commands/doctor.js +153 -24
  23. package/dist/commands/export-cmd.d.ts +4 -0
  24. package/dist/commands/export-cmd.js +66 -0
  25. package/dist/commands/import-cmd.d.ts +4 -0
  26. package/dist/commands/import-cmd.js +131 -0
  27. package/dist/commands/info.d.ts +0 -1
  28. package/dist/commands/info.js +29 -4
  29. package/dist/commands/init.d.ts +0 -1
  30. package/dist/commands/init.js +156 -117
  31. package/dist/commands/install.d.ts +1 -1
  32. package/dist/commands/install.js +118 -205
  33. package/dist/commands/list.d.ts +0 -1
  34. package/dist/commands/list.js +12 -4
  35. package/dist/commands/lock.d.ts +4 -0
  36. package/dist/commands/lock.js +171 -0
  37. package/dist/commands/optimize.d.ts +3 -0
  38. package/dist/commands/optimize.js +356 -0
  39. package/dist/commands/outdated.d.ts +4 -0
  40. package/dist/commands/outdated.js +159 -0
  41. package/dist/commands/profile.d.ts +3 -0
  42. package/dist/commands/profile.js +274 -0
  43. package/dist/commands/providers.d.ts +0 -1
  44. package/dist/commands/providers.js +1 -4
  45. package/dist/commands/recommend.d.ts +5 -0
  46. package/dist/commands/recommend.js +96 -0
  47. package/dist/commands/scan.d.ts +0 -1
  48. package/dist/commands/scan.js +13 -7
  49. package/dist/commands/search.d.ts +2 -1
  50. package/dist/commands/search.js +32 -9
  51. package/dist/commands/stats.d.ts +0 -1
  52. package/dist/commands/stats.js +83 -16
  53. package/dist/commands/team.d.ts +3 -0
  54. package/dist/commands/team.js +291 -0
  55. package/dist/commands/uninstall.d.ts +0 -1
  56. package/dist/commands/uninstall.js +18 -4
  57. package/dist/commands/update.d.ts +0 -1
  58. package/dist/commands/update.js +155 -155
  59. package/dist/commands/validate.d.ts +0 -1
  60. package/dist/commands/validate.js +14 -6
  61. package/dist/commands/verify.d.ts +4 -0
  62. package/dist/commands/verify.js +116 -0
  63. package/dist/constants.d.ts +10 -0
  64. package/dist/constants.js +13 -0
  65. package/dist/index.d.ts +0 -1
  66. package/dist/index.js +0 -1
  67. package/dist/interactive/browse.d.ts +4 -0
  68. package/dist/interactive/browse.js +103 -0
  69. package/dist/interactive/categories.d.ts +4 -0
  70. package/dist/interactive/categories.js +87 -0
  71. package/dist/interactive/health.d.ts +1 -0
  72. package/dist/interactive/health.js +57 -0
  73. package/dist/interactive/helpers.d.ts +11 -0
  74. package/dist/interactive/helpers.js +66 -0
  75. package/dist/interactive/index.d.ts +1 -0
  76. package/dist/interactive/index.js +1 -0
  77. package/dist/interactive/manage.d.ts +2 -0
  78. package/dist/interactive/manage.js +187 -0
  79. package/dist/interactive/menu.d.ts +1 -0
  80. package/dist/interactive/menu.js +107 -0
  81. package/dist/interactive/search.d.ts +2 -0
  82. package/dist/interactive/search.js +66 -0
  83. package/dist/interactive/setup.d.ts +2 -0
  84. package/dist/interactive/setup.js +48 -0
  85. package/dist/interactive/skill-detail.d.ts +5 -0
  86. package/dist/interactive/skill-detail.js +126 -0
  87. package/dist/interactive.d.ts +0 -1
  88. package/dist/interactive.js +89 -66
  89. package/dist/providers/arcana.d.ts +0 -1
  90. package/dist/providers/arcana.js +0 -1
  91. package/dist/providers/base.d.ts +0 -1
  92. package/dist/providers/base.js +0 -1
  93. package/dist/providers/github.d.ts +0 -1
  94. package/dist/providers/github.js +8 -3
  95. package/dist/registry.d.ts +0 -1
  96. package/dist/registry.js +1 -4
  97. package/dist/types.d.ts +10 -1
  98. package/dist/types.js +0 -1
  99. package/dist/utils/atomic.d.ts +0 -1
  100. package/dist/utils/atomic.js +3 -2
  101. package/dist/utils/cache.d.ts +0 -1
  102. package/dist/utils/cache.js +3 -2
  103. package/dist/utils/config.d.ts +2 -1
  104. package/dist/utils/config.js +30 -5
  105. package/dist/utils/conflict-check.d.ts +8 -0
  106. package/dist/utils/conflict-check.js +72 -0
  107. package/dist/utils/errors.d.ts +0 -1
  108. package/dist/utils/errors.js +0 -1
  109. package/dist/utils/frontmatter.d.ts +0 -1
  110. package/dist/utils/frontmatter.js +37 -10
  111. package/dist/utils/fs.d.ts +19 -1
  112. package/dist/utils/fs.js +105 -8
  113. package/dist/utils/help.d.ts +0 -1
  114. package/dist/utils/help.js +15 -28
  115. package/dist/utils/history.d.ts +0 -1
  116. package/dist/utils/history.js +0 -1
  117. package/dist/utils/http.d.ts +0 -1
  118. package/dist/utils/http.js +14 -5
  119. package/dist/utils/install-core.d.ts +48 -0
  120. package/dist/utils/install-core.js +108 -0
  121. package/dist/utils/integrity.d.ts +17 -0
  122. package/dist/utils/integrity.js +84 -0
  123. package/dist/utils/parallel.d.ts +0 -1
  124. package/dist/utils/parallel.js +0 -1
  125. package/dist/utils/project-context.d.ts +19 -0
  126. package/dist/utils/project-context.js +283 -0
  127. package/dist/utils/scanner.d.ts +0 -1
  128. package/dist/utils/scanner.js +138 -10
  129. package/dist/utils/scoring.d.ts +10 -0
  130. package/dist/utils/scoring.js +84 -0
  131. package/dist/utils/ui.d.ts +0 -1
  132. package/dist/utils/ui.js +11 -4
  133. package/dist/utils/validate.d.ts +0 -1
  134. package/dist/utils/validate.js +4 -1
  135. package/package.json +19 -7
  136. package/dist/cli.d.ts.map +0 -1
  137. package/dist/cli.js.map +0 -1
  138. package/dist/commands/audit.d.ts.map +0 -1
  139. package/dist/commands/audit.js.map +0 -1
  140. package/dist/commands/audit.test.d.ts +0 -2
  141. package/dist/commands/audit.test.d.ts.map +0 -1
  142. package/dist/commands/audit.test.js +0 -217
  143. package/dist/commands/audit.test.js.map +0 -1
  144. package/dist/commands/clean.d.ts.map +0 -1
  145. package/dist/commands/clean.js.map +0 -1
  146. package/dist/commands/config.d.ts.map +0 -1
  147. package/dist/commands/config.js.map +0 -1
  148. package/dist/commands/create.d.ts.map +0 -1
  149. package/dist/commands/create.js.map +0 -1
  150. package/dist/commands/doctor.d.ts.map +0 -1
  151. package/dist/commands/doctor.js.map +0 -1
  152. package/dist/commands/info.d.ts.map +0 -1
  153. package/dist/commands/info.js.map +0 -1
  154. package/dist/commands/init.d.ts.map +0 -1
  155. package/dist/commands/init.js.map +0 -1
  156. package/dist/commands/install.d.ts.map +0 -1
  157. package/dist/commands/install.js.map +0 -1
  158. package/dist/commands/list.d.ts.map +0 -1
  159. package/dist/commands/list.js.map +0 -1
  160. package/dist/commands/providers.d.ts.map +0 -1
  161. package/dist/commands/providers.js.map +0 -1
  162. package/dist/commands/scan.d.ts.map +0 -1
  163. package/dist/commands/scan.js.map +0 -1
  164. package/dist/commands/search.d.ts.map +0 -1
  165. package/dist/commands/search.js.map +0 -1
  166. package/dist/commands/stats.d.ts.map +0 -1
  167. package/dist/commands/stats.js.map +0 -1
  168. package/dist/commands/uninstall.d.ts.map +0 -1
  169. package/dist/commands/uninstall.js.map +0 -1
  170. package/dist/commands/update.d.ts.map +0 -1
  171. package/dist/commands/update.js.map +0 -1
  172. package/dist/commands/validate.d.ts.map +0 -1
  173. package/dist/commands/validate.js.map +0 -1
  174. package/dist/index.d.ts.map +0 -1
  175. package/dist/index.js.map +0 -1
  176. package/dist/interactive.d.ts.map +0 -1
  177. package/dist/interactive.js.map +0 -1
  178. package/dist/providers/arcana.d.ts.map +0 -1
  179. package/dist/providers/arcana.js.map +0 -1
  180. package/dist/providers/base.d.ts.map +0 -1
  181. package/dist/providers/base.js.map +0 -1
  182. package/dist/providers/github.d.ts.map +0 -1
  183. package/dist/providers/github.js.map +0 -1
  184. package/dist/registry.d.ts.map +0 -1
  185. package/dist/registry.js.map +0 -1
  186. package/dist/types.d.ts.map +0 -1
  187. package/dist/types.js.map +0 -1
  188. package/dist/utils/atomic.d.ts.map +0 -1
  189. package/dist/utils/atomic.js.map +0 -1
  190. package/dist/utils/atomic.test.d.ts +0 -2
  191. package/dist/utils/atomic.test.d.ts.map +0 -1
  192. package/dist/utils/atomic.test.js +0 -31
  193. package/dist/utils/atomic.test.js.map +0 -1
  194. package/dist/utils/cache.d.ts.map +0 -1
  195. package/dist/utils/cache.js.map +0 -1
  196. package/dist/utils/config.d.ts.map +0 -1
  197. package/dist/utils/config.js.map +0 -1
  198. package/dist/utils/config.test.d.ts +0 -2
  199. package/dist/utils/config.test.d.ts.map +0 -1
  200. package/dist/utils/config.test.js +0 -38
  201. package/dist/utils/config.test.js.map +0 -1
  202. package/dist/utils/errors.d.ts.map +0 -1
  203. package/dist/utils/errors.js.map +0 -1
  204. package/dist/utils/frontmatter.d.ts.map +0 -1
  205. package/dist/utils/frontmatter.js.map +0 -1
  206. package/dist/utils/frontmatter.test.d.ts +0 -2
  207. package/dist/utils/frontmatter.test.d.ts.map +0 -1
  208. package/dist/utils/frontmatter.test.js +0 -152
  209. package/dist/utils/frontmatter.test.js.map +0 -1
  210. package/dist/utils/fs.d.ts.map +0 -1
  211. package/dist/utils/fs.js.map +0 -1
  212. package/dist/utils/fs.test.d.ts +0 -2
  213. package/dist/utils/fs.test.d.ts.map +0 -1
  214. package/dist/utils/fs.test.js +0 -145
  215. package/dist/utils/fs.test.js.map +0 -1
  216. package/dist/utils/help.d.ts.map +0 -1
  217. package/dist/utils/help.js.map +0 -1
  218. package/dist/utils/help.test.d.ts +0 -2
  219. package/dist/utils/help.test.d.ts.map +0 -1
  220. package/dist/utils/help.test.js +0 -66
  221. package/dist/utils/help.test.js.map +0 -1
  222. package/dist/utils/history.d.ts.map +0 -1
  223. package/dist/utils/history.js.map +0 -1
  224. package/dist/utils/http.d.ts.map +0 -1
  225. package/dist/utils/http.js.map +0 -1
  226. package/dist/utils/http.test.d.ts +0 -2
  227. package/dist/utils/http.test.d.ts.map +0 -1
  228. package/dist/utils/http.test.js +0 -55
  229. package/dist/utils/http.test.js.map +0 -1
  230. package/dist/utils/parallel.d.ts.map +0 -1
  231. package/dist/utils/parallel.js.map +0 -1
  232. package/dist/utils/scanner.d.ts.map +0 -1
  233. package/dist/utils/scanner.js.map +0 -1
  234. package/dist/utils/ui.d.ts.map +0 -1
  235. package/dist/utils/ui.js.map +0 -1
  236. package/dist/utils/ui.test.d.ts +0 -2
  237. package/dist/utils/ui.test.d.ts.map +0 -1
  238. package/dist/utils/ui.test.js +0 -31
  239. package/dist/utils/ui.test.js.map +0 -1
  240. package/dist/utils/validate.d.ts.map +0 -1
  241. package/dist/utils/validate.js.map +0 -1
@@ -6,4 +6,3 @@ export declare function parseProviderSlug(input: string): {
6
6
  };
7
7
  export declare function getProvider(name?: string): Provider;
8
8
  export declare function getProviders(name?: string): Provider[];
9
- //# sourceMappingURL=registry.d.ts.map
package/dist/registry.js CHANGED
@@ -64,8 +64,5 @@ export function getProviders(name) {
64
64
  if (name)
65
65
  return [getProvider(name)];
66
66
  const config = loadConfig();
67
- return config.providers
68
- .filter((p) => p.enabled)
69
- .map((p) => createProvider(p.name, p.type, p.url));
67
+ return config.providers.filter((p) => p.enabled).map((p) => createProvider(p.name, p.type, p.url));
70
68
  }
71
- //# sourceMappingURL=registry.js.map
package/dist/types.d.ts CHANGED
@@ -4,6 +4,11 @@ export interface SkillInfo {
4
4
  version: string;
5
5
  source: string;
6
6
  repo?: string;
7
+ tags?: string[];
8
+ conflicts?: string[];
9
+ companions?: string[];
10
+ verified?: boolean;
11
+ author?: string;
7
12
  }
8
13
  export interface SkillFile {
9
14
  path: string;
@@ -26,6 +31,11 @@ export interface MarketplacePlugin {
26
31
  source: string;
27
32
  description: string;
28
33
  version: string;
34
+ tags?: string[];
35
+ conflicts?: string[];
36
+ companions?: string[];
37
+ verified?: boolean;
38
+ author?: string;
29
39
  }
30
40
  export interface ProviderConfig {
31
41
  name: string;
@@ -64,4 +74,3 @@ export interface DoctorCheck {
64
74
  message: string;
65
75
  fix?: string;
66
76
  }
67
- //# sourceMappingURL=types.d.ts.map
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types.js.map
@@ -1,2 +1 @@
1
1
  export declare function atomicWriteSync(filePath: string, content: string, mode?: number): void;
2
- //# sourceMappingURL=atomic.d.ts.map
@@ -13,8 +13,9 @@ export function atomicWriteSync(filePath, content, mode = 0o644) {
13
13
  try {
14
14
  unlinkSync(tmpPath);
15
15
  }
16
- catch { /* cleanup best-effort */ }
16
+ catch {
17
+ /* cleanup best-effort */
18
+ }
17
19
  throw err;
18
20
  }
19
21
  }
20
- //# sourceMappingURL=atomic.js.map
@@ -1,4 +1,3 @@
1
1
  export declare function readCache<T>(key: string, maxAgeMs?: number): T | null;
2
2
  export declare function writeCache<T>(key: string, data: T): void;
3
3
  export declare function clearCacheFile(key: string): void;
4
- //# sourceMappingURL=cache.d.ts.map
@@ -41,7 +41,8 @@ export function clearCacheFile(key) {
41
41
  try {
42
42
  unlinkSync(file);
43
43
  }
44
- catch { /* best-effort */ }
44
+ catch {
45
+ /* best-effort */
46
+ }
45
47
  }
46
48
  }
47
- //# sourceMappingURL=cache.js.map
@@ -1,6 +1,7 @@
1
1
  import type { ArcanaConfig, ProviderConfig } from "../types.js";
2
+ /** Validate config and return warnings for invalid fields. */
3
+ export declare function validateConfig(config: ArcanaConfig): string[];
2
4
  export declare function loadConfig(): ArcanaConfig;
3
5
  export declare function saveConfig(config: ArcanaConfig): void;
4
6
  export declare function addProvider(provider: ProviderConfig): void;
5
7
  export declare function removeProvider(name: string): boolean;
6
- //# sourceMappingURL=config.d.ts.map
@@ -4,6 +4,8 @@ import { homedir } from "node:os";
4
4
  import { ui } from "./ui.js";
5
5
  import { atomicWriteSync } from "./atomic.js";
6
6
  const CONFIG_PATH = join(homedir(), ".arcana", "config.json");
7
+ /** Matches owner/repo slug format (e.g. "medy-gribkov/arcana") */
8
+ const SLUG_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
7
9
  const DEFAULT_CONFIG = {
8
10
  defaultProvider: "arcana",
9
11
  installDir: join(homedir(), ".agents", "skills"),
@@ -17,7 +19,27 @@ const DEFAULT_CONFIG = {
17
19
  ],
18
20
  };
19
21
  function cloneConfig(config) {
20
- return { ...config, providers: config.providers.map(p => ({ ...p })) };
22
+ return { ...config, providers: config.providers.map((p) => ({ ...p })) };
23
+ }
24
+ /** Validate config and return warnings for invalid fields. */
25
+ export function validateConfig(config) {
26
+ const warnings = [];
27
+ // Validate providers have valid owner/repo slugs
28
+ for (const p of config.providers) {
29
+ if (p.type === "github" && !SLUG_RE.test(p.url)) {
30
+ warnings.push(`Provider "${p.name}" has invalid URL "${p.url}". Expected owner/repo format.`);
31
+ }
32
+ }
33
+ // Validate installDir is absolute
34
+ if (!isAbsolute(config.installDir)) {
35
+ warnings.push(`installDir "${config.installDir}" is not an absolute path.`);
36
+ }
37
+ // Validate defaultProvider matches a configured provider or valid slug
38
+ const providerNames = config.providers.map((p) => p.name);
39
+ if (!providerNames.includes(config.defaultProvider) && !SLUG_RE.test(config.defaultProvider)) {
40
+ warnings.push(`defaultProvider "${config.defaultProvider}" doesn't match any configured provider and isn't a valid slug.`);
41
+ }
42
+ return warnings;
21
43
  }
22
44
  export function loadConfig() {
23
45
  if (!existsSync(CONFIG_PATH)) {
@@ -29,7 +51,7 @@ export function loadConfig() {
29
51
  const config = {
30
52
  ...DEFAULT_CONFIG,
31
53
  ...loaded,
32
- providers: loaded.providers ?? DEFAULT_CONFIG.providers.map(p => ({ ...p })),
54
+ providers: loaded.providers ?? DEFAULT_CONFIG.providers.map((p) => ({ ...p })),
33
55
  };
34
56
  return applyEnvOverrides(config);
35
57
  }
@@ -51,11 +73,15 @@ function applyEnvOverrides(base) {
51
73
  }
52
74
  const envProvider = process.env.ARCANA_DEFAULT_PROVIDER;
53
75
  if (envProvider) {
54
- if (envProvider.trim().length === 0) {
76
+ const trimmed = envProvider.trim();
77
+ if (trimmed.length === 0) {
55
78
  console.error(ui.warn(" Warning: ARCANA_DEFAULT_PROVIDER is empty. Ignoring."));
56
79
  }
80
+ else if (!SLUG_RE.test(trimmed) && !config.providers.some((p) => p.name === trimmed)) {
81
+ console.error(ui.warn(` Warning: ARCANA_DEFAULT_PROVIDER "${trimmed}" is not a valid slug. Ignoring.`));
82
+ }
57
83
  else {
58
- config.defaultProvider = envProvider.trim();
84
+ config.defaultProvider = trimmed;
59
85
  }
60
86
  }
61
87
  return config;
@@ -87,4 +113,3 @@ export function removeProvider(name) {
87
113
  saveConfig(config);
88
114
  return true;
89
115
  }
90
- //# sourceMappingURL=config.js.map
@@ -0,0 +1,8 @@
1
+ import type { SkillInfo } from "../types.js";
2
+ import type { ProjectContext } from "./project-context.js";
3
+ export interface ConflictWarning {
4
+ type: "rule-overlap" | "skill-conflict" | "preference-clash";
5
+ message: string;
6
+ severity: "warn" | "block";
7
+ }
8
+ export declare function checkConflicts(skillName: string, skillInfo: SkillInfo | null, skillContent: string | null, context: ProjectContext): ConflictWarning[];
@@ -0,0 +1,72 @@
1
+ /** Known opposing preference pairs. If a skill promotes one, and CLAUDE.md has the other, warn. */
2
+ const OPPOSING_PAIRS = [
3
+ ["callbacks", "async/await"],
4
+ ["any", "strict typing"],
5
+ ["abbreviations", "meaningful names"],
6
+ ["classes", "functional"],
7
+ ["oop", "functional programming"],
8
+ ["semicolons", "no semicolons"],
9
+ ["tabs", "spaces"],
10
+ ];
11
+ export function checkConflicts(skillName, skillInfo, skillContent, context) {
12
+ const warnings = [];
13
+ // 1. Explicit skill-level conflicts from marketplace metadata
14
+ if (skillInfo?.conflicts && skillInfo.conflicts.length > 0) {
15
+ const installed = context.installedSkills;
16
+ const conflicting = skillInfo.conflicts.filter((c) => installed.includes(c));
17
+ for (const c of conflicting) {
18
+ warnings.push({
19
+ type: "skill-conflict",
20
+ message: `"${skillName}" conflicts with installed skill "${c}".`,
21
+ severity: "block",
22
+ });
23
+ }
24
+ }
25
+ // 2. Rule overlap: skill name matches existing .claude/rules/*.md filename
26
+ const ruleBasenames = context.ruleFiles.map((f) => f.replace(/\.md$/, "").toLowerCase());
27
+ if (ruleBasenames.includes(skillName.toLowerCase())) {
28
+ warnings.push({
29
+ type: "rule-overlap",
30
+ message: `A rule file "${skillName}.md" already exists in .claude/rules/. This skill may duplicate existing instructions.`,
31
+ severity: "warn",
32
+ });
33
+ }
34
+ // Also check if skill tags overlap heavily with rule file names
35
+ if (skillInfo?.tags) {
36
+ for (const tag of skillInfo.tags) {
37
+ if (ruleBasenames.includes(tag.toLowerCase()) && tag.toLowerCase() !== skillName.toLowerCase()) {
38
+ warnings.push({
39
+ type: "rule-overlap",
40
+ message: `Skill tag "${tag}" matches existing rule "${tag}.md". May overlap.`,
41
+ severity: "warn",
42
+ });
43
+ break; // one warning is enough
44
+ }
45
+ }
46
+ }
47
+ // 3. Preference clash: check skill content against CLAUDE.md preferences
48
+ if (skillContent && context.preferences.length > 0) {
49
+ const contentLower = skillContent.toLowerCase();
50
+ for (const [a, b] of OPPOSING_PAIRS) {
51
+ const skillHasA = contentLower.includes(a);
52
+ const skillHasB = contentLower.includes(b);
53
+ const prefsHaveA = context.preferences.some((p) => p.toLowerCase().includes(a));
54
+ const prefsHaveB = context.preferences.some((p) => p.toLowerCase().includes(b));
55
+ if (skillHasA && prefsHaveB) {
56
+ warnings.push({
57
+ type: "preference-clash",
58
+ message: `Skill mentions "${a}" but your project prefers "${b}".`,
59
+ severity: "warn",
60
+ });
61
+ }
62
+ else if (skillHasB && prefsHaveA) {
63
+ warnings.push({
64
+ type: "preference-clash",
65
+ message: `Skill mentions "${b}" but your project prefers "${a}".`,
66
+ severity: "warn",
67
+ });
68
+ }
69
+ }
70
+ }
71
+ return warnings;
72
+ }
@@ -3,4 +3,3 @@ export declare class CliError extends Error {
3
3
  readonly exitCode: number;
4
4
  constructor(message: string, code?: string, exitCode?: number);
5
5
  }
6
- //# sourceMappingURL=errors.d.ts.map
@@ -8,4 +8,3 @@ export class CliError extends Error {
8
8
  this.name = "CliError";
9
9
  }
10
10
  }
11
- //# sourceMappingURL=errors.js.map
@@ -9,4 +9,3 @@ export declare function extractFrontmatter(content: string): {
9
9
  export declare function parseFrontmatter(raw: string): SkillFrontmatter | null;
10
10
  export declare function fixSkillFrontmatter(content: string): string;
11
11
  export declare function validateSkillDir(skillDir: string, skillName: string): ValidationResult;
12
- //# sourceMappingURL=frontmatter.d.ts.map
@@ -40,7 +40,13 @@ export function parseFrontmatter(raw) {
40
40
  if (descMatch?.[1] !== undefined) {
41
41
  let value = descMatch[1].trim();
42
42
  // Handle YAML multiline: |, >, or bare indented continuation
43
- if (value === "|" || value === ">" || value === "|-" || value === "|+" || value === ">-" || value === ">+" || value === "") {
43
+ if (value === "|" ||
44
+ value === ">" ||
45
+ value === "|-" ||
46
+ value === "|+" ||
47
+ value === ">-" ||
48
+ value === ">+" ||
49
+ value === "") {
44
50
  const multilineLines = [];
45
51
  for (let j = i + 1; j < lines.length; j++) {
46
52
  const next = lines[j];
@@ -93,12 +99,7 @@ export function fixSkillFrontmatter(content) {
93
99
  if (!parsed)
94
100
  return content;
95
101
  // Rebuild clean frontmatter with only name and description
96
- const cleanFm = [
97
- FM_DELIMITER,
98
- `name: ${parsed.name}`,
99
- `description: ${parsed.description}`,
100
- FM_DELIMITER,
101
- ].join("\n");
102
+ const cleanFm = [FM_DELIMITER, `name: ${parsed.name}`, `description: ${parsed.description}`, FM_DELIMITER].join("\n");
102
103
  return cleanFm + "\n" + extracted.body.replace(/^\n+/, "\n");
103
104
  }
104
105
  export function validateSkillDir(skillDir, skillName) {
@@ -145,12 +146,29 @@ export function validateSkillDir(skillDir, skillName) {
145
146
  else if (parsed.description.length > MAX_DESC_LENGTH) {
146
147
  result.warnings.push(`Description too long (${parsed.description.length} chars, max ${MAX_DESC_LENGTH})`);
147
148
  }
148
- // Check for non-standard fields
149
+ // Check for non-standard fields (metadata is invalid per spec)
149
150
  const standardFields = ["name", "description"];
151
+ const VALID_FIELDS = [
152
+ "name",
153
+ "description",
154
+ "argument-hint",
155
+ "compatibility",
156
+ "disable-model-invocation",
157
+ "license",
158
+ "user-invokable",
159
+ ];
150
160
  for (const line of extracted.raw.split("\n")) {
151
161
  const keyMatch = line.match(/^(\w[\w-]*):/);
152
162
  if (keyMatch?.[1] && !standardFields.includes(keyMatch[1])) {
153
- result.infos.push(`Non-standard field: ${keyMatch[1]}`);
163
+ if (keyMatch[1] === "metadata") {
164
+ result.warnings.push("Invalid field: metadata (not allowed in frontmatter)");
165
+ }
166
+ else if (!VALID_FIELDS.includes(keyMatch[1])) {
167
+ result.infos.push(`Non-standard field: ${keyMatch[1]}`);
168
+ }
169
+ else {
170
+ result.infos.push(`Optional field: ${keyMatch[1]}`);
171
+ }
154
172
  }
155
173
  }
156
174
  if (parsed.name !== skillName) {
@@ -165,8 +183,17 @@ export function validateSkillDir(skillDir, skillName) {
165
183
  if (extracted.body.trim().length >= 50 && !extracted.body.includes("##")) {
166
184
  result.infos.push("Body has no ## headings (recommended for structure)");
167
185
  }
186
+ // Check for code blocks (quality signal)
187
+ if (extracted.body.trim().length >= 50 && !extracted.body.includes("```")) {
188
+ result.infos.push("No code blocks found (procedural skills should include code examples)");
189
+ }
190
+ // Check for BAD/GOOD pattern examples
191
+ const hasPattern = /(?:BAD|GOOD|WRONG|RIGHT|AVOID|PREFER|DO NOT|INSTEAD)/i.test(extracted.body) ||
192
+ /<!--\s*(?:bad|good)\s*-->/i.test(extracted.body);
193
+ if (extracted.body.trim().length >= 100 && !hasPattern) {
194
+ result.infos.push("No BAD/GOOD contrast patterns found (recommended for teaching)");
195
+ }
168
196
  if (result.errors.length > 0)
169
197
  result.valid = false;
170
198
  return result;
171
199
  }
172
- //# sourceMappingURL=frontmatter.js.map
@@ -12,5 +12,23 @@ export interface SymlinkInfo {
12
12
  target: string;
13
13
  broken: boolean;
14
14
  }
15
+ /**
16
+ * List files in a directory matching a pattern, optionally filtered by age.
17
+ */
18
+ export declare function listFilesByAge(dir: string, ext: string, olderThanDays: number): {
19
+ path: string;
20
+ sizeMB: number;
21
+ daysOld: number;
22
+ }[];
23
+ /**
24
+ * Check if a Claude project directory's source project still exists on disk.
25
+ *
26
+ * Claude Code encodes paths like "c--Users-User-Coding-Personal-arcana" where
27
+ * hyphens replace path separators. This is ambiguous when folder names contain
28
+ * hyphens (e.g., "lead-scraper" becomes indistinguishable from "lead/scraper").
29
+ *
30
+ * Strategy: try all possible split points to find a path that exists on disk.
31
+ * If ANY interpretation resolves to a real directory, the project is not orphaned.
32
+ */
33
+ export declare function isOrphanedProject(projectDirName: string): boolean;
15
34
  export declare function listSymlinks(): SymlinkInfo[];
16
- //# sourceMappingURL=fs.d.ts.map
package/dist/utils/fs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, renameSync, lstatSync, readlinkSync } from "node:fs";
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, renameSync, lstatSync, readlinkSync, } from "node:fs";
2
2
  import { join, dirname, resolve, sep } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { loadConfig } from "../utils/config.js";
@@ -26,10 +26,14 @@ export function getDirSize(dir) {
26
26
  else
27
27
  size += stat.size;
28
28
  }
29
- catch { /* skip unreadable entries */ }
29
+ catch {
30
+ /* skip unreadable entries */
31
+ }
30
32
  }
31
33
  }
32
- catch { /* skip unreadable dirs */ }
34
+ catch {
35
+ /* skip unreadable dirs */
36
+ }
33
37
  }
34
38
  return size;
35
39
  }
@@ -45,14 +49,18 @@ export function installSkill(skillName, files) {
45
49
  try {
46
50
  for (const file of files) {
47
51
  // Reject paths containing .. before resolving
48
- if (file.path.includes("..") || file.path.includes("~") || file.path.startsWith("\\\\") || file.path.startsWith("//")) {
52
+ if (file.path.includes("..") ||
53
+ file.path.includes("~") ||
54
+ file.path.startsWith("\\\\") ||
55
+ file.path.startsWith("//")) {
49
56
  throw new Error(`Path traversal blocked: ${file.path}`);
50
57
  }
51
58
  const filePath = resolve(tempDir, file.path);
52
59
  // Normalize to lowercase on Windows for case-insensitive comparison
53
60
  const normalizedFile = process.platform === "win32" ? filePath.toLowerCase() : filePath;
54
61
  const normalizedTemp = process.platform === "win32" ? (tempDir + sep).toLowerCase() : tempDir + sep;
55
- if (!normalizedFile.startsWith(normalizedTemp) && normalizedFile !== (process.platform === "win32" ? tempDir.toLowerCase() : tempDir)) {
62
+ if (!normalizedFile.startsWith(normalizedTemp) &&
63
+ normalizedFile !== (process.platform === "win32" ? tempDir.toLowerCase() : tempDir)) {
56
64
  throw new Error(`Path traversal blocked: ${file.path}`);
57
65
  }
58
66
  const dir = dirname(filePath);
@@ -72,7 +80,9 @@ export function installSkill(skillName, files) {
72
80
  try {
73
81
  rmSync(tempDir, { recursive: true, force: true });
74
82
  }
75
- catch { /* best-effort */ }
83
+ catch {
84
+ /* best-effort */
85
+ }
76
86
  throw err;
77
87
  }
78
88
  return skillDir;
@@ -97,6 +107,92 @@ export function writeSkillMeta(skillName, meta) {
97
107
  mkdirSync(skillDir, { recursive: true });
98
108
  atomicWriteSync(join(skillDir, META_FILE), JSON.stringify(meta, null, 2) + "\n", 0o644);
99
109
  }
110
+ /**
111
+ * List files in a directory matching a pattern, optionally filtered by age.
112
+ */
113
+ export function listFilesByAge(dir, ext, olderThanDays) {
114
+ if (!existsSync(dir))
115
+ return [];
116
+ const results = [];
117
+ const now = Date.now();
118
+ const cutoff = olderThanDays * 24 * 60 * 60 * 1000;
119
+ const queue = [dir];
120
+ while (queue.length > 0) {
121
+ const current = queue.pop();
122
+ try {
123
+ for (const entry of readdirSync(current)) {
124
+ const full = join(current, entry);
125
+ try {
126
+ const stat = lstatSync(full);
127
+ if (stat.isSymbolicLink())
128
+ continue;
129
+ if (stat.isDirectory()) {
130
+ queue.push(full);
131
+ continue;
132
+ }
133
+ if (ext && !entry.endsWith(ext))
134
+ continue;
135
+ const age = now - stat.mtimeMs;
136
+ if (age > cutoff) {
137
+ results.push({
138
+ path: full,
139
+ sizeMB: stat.size / (1024 * 1024),
140
+ daysOld: Math.floor(age / (24 * 60 * 60 * 1000)),
141
+ });
142
+ }
143
+ }
144
+ catch {
145
+ /* skip */
146
+ }
147
+ }
148
+ }
149
+ catch {
150
+ /* skip */
151
+ }
152
+ }
153
+ return results;
154
+ }
155
+ /**
156
+ * Check if a Claude project directory's source project still exists on disk.
157
+ *
158
+ * Claude Code encodes paths like "c--Users-User-Coding-Personal-arcana" where
159
+ * hyphens replace path separators. This is ambiguous when folder names contain
160
+ * hyphens (e.g., "lead-scraper" becomes indistinguishable from "lead/scraper").
161
+ *
162
+ * Strategy: try all possible split points to find a path that exists on disk.
163
+ * If ANY interpretation resolves to a real directory, the project is not orphaned.
164
+ */
165
+ export function isOrphanedProject(projectDirName) {
166
+ const match = projectDirName.match(/^([A-Za-z])--(.+)$/);
167
+ if (!match)
168
+ return false; // can't decode, assume not orphaned
169
+ const drive = match[1].toUpperCase();
170
+ const rest = match[2];
171
+ const parts = rest.split("-");
172
+ // Try building paths by joining consecutive parts with hyphens vs separators.
173
+ // Optimization: just check if the encoded dir name prefix matches any existing path.
174
+ // Build all possible paths by combining adjacent segments.
175
+ function tryPaths(segments, depth, currentPath) {
176
+ if (depth >= segments.length) {
177
+ return existsSync(currentPath);
178
+ }
179
+ // Try consuming 1, 2, 3... segments as a single directory name
180
+ const sep = process.platform === "win32" ? "\\" : "/";
181
+ for (let take = 1; take <= segments.length - depth; take++) {
182
+ const dirName = segments.slice(depth, depth + take).join("-");
183
+ const next = currentPath + sep + dirName;
184
+ // Prune: if this intermediate path doesn't exist, no point going deeper
185
+ // (only prune when we still have more segments to process)
186
+ if (depth + take < segments.length && !existsSync(next))
187
+ continue;
188
+ if (tryPaths(segments, depth + take, next))
189
+ return true;
190
+ }
191
+ return false;
192
+ }
193
+ const root = process.platform === "win32" ? `${drive}:` : `/${drive.toLowerCase()}`;
194
+ return !tryPaths(parts, 0, root);
195
+ }
100
196
  export function listSymlinks() {
101
197
  const symlinkDir = join(homedir(), ".claude", "skills");
102
198
  if (!existsSync(symlinkDir))
@@ -111,8 +207,9 @@ export function listSymlinks() {
111
207
  results.push({ name: entry, fullPath, target, broken: !existsSync(target) });
112
208
  }
113
209
  }
114
- catch { /* skip unreadable */ }
210
+ catch {
211
+ /* skip unreadable */
212
+ }
115
213
  }
116
214
  return results;
117
215
  }
118
- //# sourceMappingURL=fs.js.map
@@ -3,4 +3,3 @@ export declare function buildCustomHelp(version: string): string;
3
3
  export declare function isFirstRun(): boolean;
4
4
  export declare function markInitialized(): void;
5
5
  export declare function showWelcome(version: string): void;
6
- //# sourceMappingURL=help.d.ts.map
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import * as p from "@clack/prompts";
5
5
  import chalk from "chalk";
6
6
  import { ui } from "./ui.js";
7
+ import { getGroupedCommands } from "../command-registry.js";
7
8
  const noColor = !!(process.env.NO_COLOR || process.env.TERM === "dumb");
8
9
  function amberShade(hex, text) {
9
10
  if (noColor)
@@ -32,30 +33,12 @@ export function renderBanner() {
32
33
  }
33
34
  return BANNER_LINES.map((line, i) => ` ${amberShade(AMBER_HEXES[i], line)}`).join("\n");
34
35
  }
35
- const COMMAND_GROUPS = {
36
- "GETTING STARTED": [
37
- { cmd: "init", desc: "Initialize arcana in current project" },
38
- { cmd: "doctor", desc: "Check environment and diagnose issues" },
39
- ],
40
- SKILLS: [
41
- { cmd: "list", desc: "List available skills" },
42
- { cmd: "search <query>", desc: "Search across providers" },
43
- { cmd: "info <skill>", desc: "Show skill details" },
44
- { cmd: "install [skills...]", desc: "Install one or more skills" },
45
- { cmd: "update [skills...]", desc: "Update installed skills" },
46
- { cmd: "uninstall [skills...]", desc: "Remove one or more skills" },
47
- ],
48
- DEVELOPMENT: [
49
- { cmd: "create <name>", desc: "Create a new skill from template" },
50
- { cmd: "validate [skill]", desc: "Validate skill structure" },
51
- { cmd: "audit [skill]", desc: "Audit skill quality" },
52
- ],
53
- CONFIGURATION: [
54
- { cmd: "config [key] [val]", desc: "View or modify configuration" },
55
- { cmd: "providers", desc: "Manage skill providers" },
56
- { cmd: "clean", desc: "Remove orphaned data" },
57
- { cmd: "stats", desc: "Show session analytics" },
58
- ],
36
+ // Help groups: subset of registry for --help display (keeps output scannable)
37
+ const HELP_GROUPS = {
38
+ "GETTING STARTED": ["init", "doctor"],
39
+ SKILLS: ["list", "search", "info", "install", "update", "uninstall", "recommend"],
40
+ DEVELOPMENT: ["create", "validate", "audit"],
41
+ CONFIGURATION: ["config", "providers", "clean", "stats"],
59
42
  };
60
43
  const EXAMPLES = [
61
44
  "$ arcana install code-reviewer typescript golang",
@@ -74,11 +57,16 @@ export function buildCustomHelp(version) {
74
57
  lines.push("");
75
58
  lines.push(` ${ui.dim("USAGE")}`);
76
59
  lines.push(" arcana <command> [options]");
77
- for (const [group, commands] of Object.entries(COMMAND_GROUPS)) {
60
+ const allCommands = getGroupedCommands();
61
+ const allFlat = Object.values(allCommands).flat();
62
+ for (const [group, names] of Object.entries(HELP_GROUPS)) {
78
63
  lines.push("");
79
64
  lines.push(` ${ui.dim(group)}`);
80
- for (const { cmd, desc } of commands) {
81
- lines.push(` ${ui.cyan(padRight(cmd, 22))}${ui.dim(desc)}`);
65
+ for (const name of names) {
66
+ const entry = allFlat.find((c) => c.name === name);
67
+ if (!entry)
68
+ continue;
69
+ lines.push(` ${ui.cyan(padRight(entry.usage, 22))}${ui.dim(entry.description)}`);
82
70
  }
83
71
  }
84
72
  lines.push("");
@@ -114,4 +102,3 @@ export function showWelcome(version) {
114
102
  p.log.info("They install on-demand and only load when relevant, not all at once.");
115
103
  console.log();
116
104
  }
117
- //# sourceMappingURL=help.js.map
@@ -7,4 +7,3 @@ export declare function readHistory(): HistoryEntry[];
7
7
  export declare function appendHistory(action: string, target?: string): void;
8
8
  export declare function clearHistory(): void;
9
9
  export declare function getRecentSkills(limit?: number): string[];
10
- //# sourceMappingURL=history.d.ts.map
@@ -55,4 +55,3 @@ export function getRecentSkills(limit = 5) {
55
55
  }
56
56
  return skills;
57
57
  }
58
- //# sourceMappingURL=history.js.map
@@ -14,4 +14,3 @@ export declare class RateLimitError extends HttpError {
14
14
  constructor(url: string, resetAt: Date | null);
15
15
  }
16
16
  export declare function httpGet(url: string, timeout?: number): Promise<HttpResponse>;
17
- //# sourceMappingURL=http.d.ts.map