@sporesec/arcana 3.0.3 → 4.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 (102) hide show
  1. package/dist/cli.js +25 -298
  2. package/dist/command-defs.d.ts +28 -0
  3. package/dist/command-defs.js +414 -0
  4. package/dist/commands/audit.js +18 -4
  5. package/dist/commands/clean.d.ts +1 -0
  6. package/dist/commands/clean.js +80 -0
  7. package/dist/commands/compress.d.ts +5 -0
  8. package/dist/commands/compress.js +38 -0
  9. package/dist/commands/config.js +40 -26
  10. package/dist/commands/create.js +2 -0
  11. package/dist/commands/curate.d.ts +39 -0
  12. package/dist/commands/curate.js +222 -0
  13. package/dist/commands/diff.js +2 -0
  14. package/dist/commands/doctor.d.ts +1 -0
  15. package/dist/commands/doctor.js +61 -2
  16. package/dist/commands/import-cmd.js +5 -0
  17. package/dist/commands/index.d.ts +5 -0
  18. package/dist/commands/index.js +107 -0
  19. package/dist/commands/info.js +19 -8
  20. package/dist/commands/init.d.ts +3 -0
  21. package/dist/commands/init.js +71 -0
  22. package/dist/commands/install.js +2 -0
  23. package/dist/commands/list.js +8 -0
  24. package/dist/commands/load.d.ts +10 -0
  25. package/dist/commands/load.js +130 -0
  26. package/dist/commands/lock.js +35 -24
  27. package/dist/commands/mcp.d.ts +4 -0
  28. package/dist/commands/mcp.js +87 -0
  29. package/dist/commands/outdated.js +8 -6
  30. package/dist/commands/providers.js +29 -21
  31. package/dist/commands/recommend.js +11 -3
  32. package/dist/commands/remember.d.ts +12 -0
  33. package/dist/commands/remember.js +111 -0
  34. package/dist/commands/scan.d.ts +2 -0
  35. package/dist/commands/scan.js +46 -8
  36. package/dist/commands/search.js +6 -0
  37. package/dist/commands/uninstall.js +36 -0
  38. package/dist/commands/update.js +27 -0
  39. package/dist/commands/validate.js +8 -0
  40. package/dist/commands/verify.js +2 -0
  41. package/dist/compress/engine.d.ts +21 -0
  42. package/dist/compress/engine.js +106 -0
  43. package/dist/compress/index.d.ts +7 -0
  44. package/dist/compress/index.js +10 -0
  45. package/dist/compress/rules/generic.d.ts +1 -0
  46. package/dist/compress/rules/generic.js +9 -0
  47. package/dist/compress/rules/git.d.ts +1 -0
  48. package/dist/compress/rules/git.js +113 -0
  49. package/dist/compress/rules/npm.d.ts +1 -0
  50. package/dist/compress/rules/npm.js +99 -0
  51. package/dist/compress/rules/test-runner.d.ts +1 -0
  52. package/dist/compress/rules/test-runner.js +103 -0
  53. package/dist/compress/rules/tsc.d.ts +1 -0
  54. package/dist/compress/rules/tsc.js +39 -0
  55. package/dist/compress/tracker.d.ts +16 -0
  56. package/dist/compress/tracker.js +45 -0
  57. package/dist/constants.d.ts +12 -0
  58. package/dist/constants.js +29 -0
  59. package/dist/interactive/helpers.js +1 -0
  60. package/dist/interactive/menu.js +6 -1
  61. package/dist/interactive/optimize-flow.js +4 -4
  62. package/dist/mcp/install.d.ts +10 -0
  63. package/dist/mcp/install.js +109 -0
  64. package/dist/mcp/registry.d.ts +11 -0
  65. package/dist/mcp/registry.js +27 -0
  66. package/dist/providers/anthropics.d.ts +4 -0
  67. package/dist/providers/anthropics.js +10 -0
  68. package/dist/registry.js +4 -0
  69. package/dist/session/trim.d.ts +23 -0
  70. package/dist/session/trim.js +132 -0
  71. package/dist/utils/cache.js +2 -2
  72. package/dist/utils/config.d.ts +2 -0
  73. package/dist/utils/config.js +33 -14
  74. package/dist/utils/help.js +16 -8
  75. package/dist/utils/install-core.js +23 -1
  76. package/dist/utils/memory.d.ts +25 -0
  77. package/dist/utils/memory.js +103 -0
  78. package/dist/utils/project-context.js +4 -0
  79. package/dist/utils/scanner.d.ts +22 -1
  80. package/dist/utils/scanner.js +81 -9
  81. package/dist/utils/sessions.d.ts +2 -0
  82. package/dist/utils/sessions.js +36 -0
  83. package/dist/utils/ui.js +5 -0
  84. package/dist/utils/usage.d.ts +17 -0
  85. package/dist/utils/usage.js +83 -0
  86. package/package.json +42 -7
  87. package/dist/command-registry.d.ts +0 -10
  88. package/dist/command-registry.js +0 -65
  89. package/dist/commands/benchmark.d.ts +0 -4
  90. package/dist/commands/benchmark.js +0 -178
  91. package/dist/commands/compact.d.ts +0 -6
  92. package/dist/commands/compact.js +0 -239
  93. package/dist/commands/optimize.d.ts +0 -3
  94. package/dist/commands/optimize.js +0 -356
  95. package/dist/commands/profile.d.ts +0 -3
  96. package/dist/commands/profile.js +0 -274
  97. package/dist/commands/stats.d.ts +0 -3
  98. package/dist/commands/stats.js +0 -210
  99. package/dist/commands/team.d.ts +0 -3
  100. package/dist/commands/team.js +0 -291
  101. package/dist/interactive.d.ts +0 -1
  102. package/dist/interactive.js +0 -841
@@ -1,9 +1,49 @@
1
- import { existsSync, readdirSync, readFileSync, statSync, openSync, readSync, closeSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, statSync, openSync, readSync, closeSync, mkdirSync, rmSync, writeFileSync, } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { execSync } from "node:child_process";
5
5
  import { ui, banner, suggest } from "../utils/ui.js";
6
6
  import { getInstallDir, getDirSize, listSymlinks, isOrphanedProject } from "../utils/fs.js";
7
+ /** Auto-fix a doctor check. Returns true if fixed. */
8
+ function autoFix(check) {
9
+ try {
10
+ switch (check.name) {
11
+ case "Skills directory": {
12
+ const dir = getInstallDir();
13
+ if (!existsSync(dir)) {
14
+ mkdirSync(dir, { recursive: true });
15
+ return true;
16
+ }
17
+ return false;
18
+ }
19
+ case "Symlinks": {
20
+ const broken = listSymlinks().filter((s) => s.broken);
21
+ for (const s of broken) {
22
+ try {
23
+ rmSync(s.fullPath);
24
+ }
25
+ catch {
26
+ /* skip */
27
+ }
28
+ }
29
+ return broken.length > 0;
30
+ }
31
+ case "Arcana config": {
32
+ const configPath = join(homedir(), ".arcana", "config.json");
33
+ const dir = join(homedir(), ".arcana");
34
+ if (!existsSync(dir))
35
+ mkdirSync(dir, { recursive: true });
36
+ writeFileSync(configPath, JSON.stringify({ defaultProvider: "arcana", providers: [] }, null, 2));
37
+ return true;
38
+ }
39
+ default:
40
+ return false;
41
+ }
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
7
47
  function checkNodeVersion() {
8
48
  const major = parseInt(process.version.slice(1));
9
49
  if (major >= 18) {
@@ -300,10 +340,12 @@ export function runDoctorChecks() {
300
340
  ];
301
341
  }
302
342
  export async function doctorCommand(opts = {}) {
343
+ /* v8 ignore start */
303
344
  if (!opts.json) {
304
345
  banner();
305
346
  console.log(ui.bold(" Environment Health Check\n"));
306
347
  }
348
+ /* v8 ignore stop */
307
349
  const checks = runDoctorChecks();
308
350
  if (opts.json) {
309
351
  console.log(JSON.stringify({
@@ -316,16 +358,32 @@ export async function doctorCommand(opts = {}) {
316
358
  }, null, 2));
317
359
  return;
318
360
  }
361
+ /* v8 ignore start */
362
+ let fixed = 0;
319
363
  for (const check of checks) {
320
364
  const icon = check.status === "pass" ? ui.success("[OK]") : check.status === "warn" ? ui.warn("[!!]") : ui.error("[XX]");
321
365
  console.log(` ${icon} ${ui.bold(check.name)}: ${check.message}`);
322
- if (check.fix) {
366
+ // Auto-fix if --fix flag and check has issues
367
+ if (opts.fix && check.status !== "pass" && check.fix) {
368
+ const wasFixed = autoFix(check);
369
+ if (wasFixed) {
370
+ console.log(ui.success(` Fixed automatically`));
371
+ fixed++;
372
+ }
373
+ else {
374
+ console.log(ui.dim(` Manual fix: ${check.fix}`));
375
+ }
376
+ }
377
+ else if (check.fix && !opts.fix) {
323
378
  console.log(ui.dim(` Fix: ${check.fix}`));
324
379
  }
325
380
  }
326
381
  const fails = checks.filter((c) => c.status === "fail").length;
327
382
  const warns = checks.filter((c) => c.status === "warn").length;
328
383
  console.log();
384
+ if (opts.fix && fixed > 0) {
385
+ console.log(ui.success(` Auto-fixed ${fixed} issue${fixed > 1 ? "s" : ""}`));
386
+ }
329
387
  if (fails > 0) {
330
388
  console.log(ui.error(` ${fails} issue${fails > 1 ? "s" : ""} found`));
331
389
  }
@@ -339,4 +397,5 @@ export async function doctorCommand(opts = {}) {
339
397
  if (fails === 0 && warns === 0) {
340
398
  suggest("arcana list");
341
399
  }
400
+ /* v8 ignore stop */
342
401
  }
@@ -33,6 +33,7 @@ export async function importCommand(file, opts) {
33
33
  console.log(JSON.stringify({ error: `File not found: ${file}` }));
34
34
  }
35
35
  else {
36
+ /* v8 ignore next */
36
37
  console.error(`Error: File not found: ${file}`);
37
38
  }
38
39
  process.exit(1);
@@ -76,6 +77,7 @@ export async function importCommand(file, opts) {
76
77
  failed.push(entry.name);
77
78
  errors[entry.name] = msg;
78
79
  if (!opts.json) {
80
+ /* v8 ignore next */
79
81
  console.error(`Skipping ${entry.name}: ${msg}`);
80
82
  }
81
83
  continue;
@@ -105,6 +107,7 @@ export async function importCommand(file, opts) {
105
107
  updateLockEntry(entry.name, version, provider.name, files);
106
108
  installed.push(entry.name);
107
109
  if (!opts.json) {
110
+ /* v8 ignore next */
108
111
  console.log(`Installed ${entry.name}`);
109
112
  }
110
113
  }
@@ -113,6 +116,7 @@ export async function importCommand(file, opts) {
113
116
  failed.push(entry.name);
114
117
  errors[entry.name] = msg;
115
118
  if (!opts.json) {
119
+ /* v8 ignore next */
116
120
  console.error(`Failed to install ${entry.name}: ${msg}`);
117
121
  }
118
122
  }
@@ -124,6 +128,7 @@ export async function importCommand(file, opts) {
124
128
  console.log(JSON.stringify(result));
125
129
  }
126
130
  else {
131
+ /* v8 ignore next */
127
132
  console.log(`Import complete: ${installed.length} installed, ${skipped.length} skipped, ${failed.length} failed`);
128
133
  }
129
134
  if (failed.length > 0)
@@ -0,0 +1,5 @@
1
+ /** Regenerate the skill index file. Returns the number of skills indexed. */
2
+ export declare function regenerateIndex(): number;
3
+ export declare function indexCommand(opts: {
4
+ json?: boolean;
5
+ }): Promise<void>;
@@ -0,0 +1,107 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getInstallDir } from "../utils/fs.js";
4
+ import { atomicWriteSync } from "../utils/atomic.js";
5
+ import { INDEX_FILENAME } from "../constants.js";
6
+ import { ui, banner } from "../utils/ui.js";
7
+ /** Parse frontmatter description from a SKILL.md file. */
8
+ function extractDescription(skillDir) {
9
+ const skillMd = join(skillDir, "SKILL.md");
10
+ if (!existsSync(skillMd))
11
+ return "";
12
+ try {
13
+ const content = readFileSync(skillMd, "utf-8");
14
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
15
+ if (!match)
16
+ return "";
17
+ const fmLine = match[1].split("\n").find((l) => l.startsWith("description:"));
18
+ if (!fmLine)
19
+ return "";
20
+ // Strip quotes and trim
21
+ return fmLine
22
+ .replace(/^description:\s*/, "")
23
+ .replace(/^["']|["']$/g, "")
24
+ .trim();
25
+ }
26
+ catch {
27
+ return "";
28
+ }
29
+ }
30
+ /** Collect all installed skills with their descriptions. */
31
+ function collectSkillEntries() {
32
+ const installDir = getInstallDir();
33
+ if (!existsSync(installDir))
34
+ return [];
35
+ const entries = [];
36
+ for (const name of readdirSync(installDir).sort()) {
37
+ const skillDir = join(installDir, name);
38
+ try {
39
+ if (!statSync(skillDir).isDirectory())
40
+ continue;
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ // Skip index and loaded files
46
+ if (name.startsWith("_"))
47
+ continue;
48
+ const description = extractDescription(skillDir);
49
+ entries.push({ name, description });
50
+ }
51
+ return entries;
52
+ }
53
+ /** Generate the index markdown content. */
54
+ function generateIndexContent(entries) {
55
+ const lines = [];
56
+ lines.push(`# Installed Skills (${entries.length})`);
57
+ lines.push("");
58
+ lines.push("| Skill | Description |");
59
+ lines.push("|-------|-------------|");
60
+ for (const entry of entries) {
61
+ // Truncate description to keep index compact
62
+ const desc = entry.description.length > 120 ? entry.description.slice(0, 117) + "..." : entry.description;
63
+ lines.push(`| ${entry.name} | ${desc} |`);
64
+ }
65
+ lines.push("");
66
+ lines.push("To load a skill into context: `arcana load <skill-name>`");
67
+ lines.push("");
68
+ return lines.join("\n");
69
+ }
70
+ /** Regenerate the skill index file. Returns the number of skills indexed. */
71
+ export function regenerateIndex() {
72
+ const entries = collectSkillEntries();
73
+ const installDir = getInstallDir();
74
+ if (!existsSync(installDir))
75
+ return 0;
76
+ const indexPath = join(installDir, INDEX_FILENAME);
77
+ const content = generateIndexContent(entries);
78
+ atomicWriteSync(indexPath, content, 0o644);
79
+ return entries.length;
80
+ }
81
+ export async function indexCommand(opts) {
82
+ if (!opts.json) {
83
+ banner();
84
+ console.log(ui.bold(" Skill Index\n"));
85
+ }
86
+ const count = regenerateIndex();
87
+ const installDir = getInstallDir();
88
+ const indexPath = join(installDir, INDEX_FILENAME);
89
+ if (opts.json) {
90
+ console.log(JSON.stringify({
91
+ indexed: count,
92
+ path: indexPath,
93
+ }));
94
+ return;
95
+ }
96
+ if (count === 0) {
97
+ console.log(ui.dim(" No skills installed."));
98
+ }
99
+ else {
100
+ console.log(ui.success(` [OK]`) + ` Indexed ${count} skills`);
101
+ console.log(ui.dim(` Path: ${indexPath}`));
102
+ console.log();
103
+ console.log(ui.dim(" Agents load this index instead of all skills at once."));
104
+ console.log(ui.dim(" Use arcana load <skill> to load full skill content on demand."));
105
+ }
106
+ console.log();
107
+ }
@@ -3,23 +3,27 @@ import { isSkillInstalled, readSkillMeta } from "../utils/fs.js";
3
3
  import { getProviders } from "../registry.js";
4
4
  import { validateSlug } from "../utils/validate.js";
5
5
  export async function infoCommand(skillName, opts) {
6
+ /* v8 ignore start */
6
7
  if (!opts.json) {
7
8
  banner();
8
9
  }
10
+ /* v8 ignore stop */
9
11
  try {
10
12
  validateSlug(skillName, "skill name");
11
13
  }
12
14
  catch (err) {
13
15
  if (opts.json) {
14
16
  console.log(JSON.stringify({ error: err instanceof Error ? err.message : "Invalid skill name" }));
17
+ process.exit(1);
15
18
  }
16
- else {
17
- console.log(ui.error(` ${err instanceof Error ? err.message : "Invalid skill name"}`));
18
- console.log();
19
- }
19
+ /* v8 ignore start */
20
+ console.log(ui.error(` ${err instanceof Error ? err.message : "Invalid skill name"}`));
21
+ console.log();
20
22
  process.exit(1);
23
+ /* v8 ignore stop */
21
24
  }
22
25
  const providers = getProviders(opts.provider);
26
+ /* v8 ignore next */
23
27
  const s = opts.json ? noopSpinner() : spinner(`Looking up ${ui.bold(skillName)}...`);
24
28
  s.start();
25
29
  try {
@@ -48,6 +52,7 @@ export async function infoCommand(skillName, opts) {
48
52
  }));
49
53
  return;
50
54
  }
55
+ /* v8 ignore start */
51
56
  console.log(ui.bold(` ${skill.name}`) + ui.dim(` v${skill.version}`));
52
57
  if (installed) {
53
58
  const meta = readSkillMeta(skillName);
@@ -86,6 +91,7 @@ export async function infoCommand(skillName, opts) {
86
91
  console.log(ui.dim(` Install: `) + ui.cyan(`arcana install ${skill.name}`));
87
92
  console.log();
88
93
  return;
94
+ /* v8 ignore stop */
89
95
  }
90
96
  }
91
97
  }
@@ -107,6 +113,7 @@ export async function infoCommand(skillName, opts) {
107
113
  }));
108
114
  return;
109
115
  }
116
+ /* v8 ignore start */
110
117
  console.log(ui.warn(" Showing cached data (offline)"));
111
118
  console.log();
112
119
  console.log(ui.bold(` ${skillName}`) + ui.dim(` v${meta?.version ?? "unknown"}`));
@@ -119,21 +126,25 @@ export async function infoCommand(skillName, opts) {
119
126
  console.log(ui.dim(` Source: ${meta?.source ?? "local"}`));
120
127
  console.log();
121
128
  return;
129
+ /* v8 ignore stop */
122
130
  }
123
131
  if (opts.json) {
124
132
  console.log(JSON.stringify({ error: err instanceof Error ? err.message : "Lookup failed" }));
125
133
  process.exit(1);
126
134
  }
135
+ /* v8 ignore start */
127
136
  s.fail("Lookup failed due to a network or provider error.");
128
137
  printErrorWithHint(err, true);
129
138
  process.exit(1);
139
+ /* v8 ignore stop */
130
140
  }
131
141
  if (opts.json) {
132
142
  console.log(JSON.stringify({ error: `Skill "${skillName}" not found` }));
143
+ process.exit(1);
133
144
  }
134
- else {
135
- s.fail(`Skill "${skillName}" not found`);
136
- console.log();
137
- }
145
+ /* v8 ignore start */
146
+ s.fail(`Skill "${skillName}" not found`);
147
+ console.log();
138
148
  process.exit(1);
149
+ /* v8 ignore stop */
139
150
  }
@@ -1,9 +1,12 @@
1
+ type ToolName = "claude" | "cursor" | "codex" | "gemini" | "antigravity" | "windsurf" | "aider";
1
2
  interface ProjectInfo {
2
3
  name: string;
3
4
  type: string;
4
5
  lang: string;
5
6
  }
6
7
  export declare function detectProject(cwd: string): ProjectInfo;
8
+ /** Detect which AI tools are already configured in this project. */
9
+ export declare function detectInstalledTools(cwd: string): ToolName[];
7
10
  export declare const SKILL_SUGGESTIONS: Record<string, string[]>;
8
11
  export declare const SKILL_SUGGESTIONS_DEFAULT: string[];
9
12
  export declare function initCommand(opts: {
@@ -8,6 +8,26 @@ export function detectProject(cwd) {
8
8
  const ctx = detectProjectContext(cwd);
9
9
  return { name: ctx.name, type: ctx.type, lang: ctx.lang };
10
10
  }
11
+ /** Detect which AI tools are already configured in this project. */
12
+ export function detectInstalledTools(cwd) {
13
+ const tools = [];
14
+ if (existsSync(join(cwd, "CLAUDE.md")))
15
+ tools.push("claude");
16
+ if (existsSync(join(cwd, ".cursor")))
17
+ tools.push("cursor");
18
+ if (existsSync(join(cwd, "AGENTS.md")))
19
+ tools.push("codex");
20
+ if (existsSync(join(cwd, "GEMINI.md")))
21
+ tools.push("gemini");
22
+ if (existsSync(join(cwd, ".windsurfrules")))
23
+ tools.push("windsurf");
24
+ if (existsSync(join(cwd, "AGENT.md")))
25
+ tools.push("antigravity");
26
+ if (existsSync(join(cwd, ".aider.conf.yml")))
27
+ tools.push("aider");
28
+ return tools;
29
+ }
30
+ /* v8 ignore start -- template functions used only in interactive initCommand */
11
31
  function claudeTemplate(proj) {
12
32
  return `# CLAUDE.md - ${proj.name}
13
33
 
@@ -15,6 +35,12 @@ function claudeTemplate(proj) {
15
35
  - **Type:** ${proj.type}
16
36
  - **Language:** ${proj.lang}
17
37
 
38
+ ## Skills
39
+ Active skills curated at ~/.agents/skills/_active.md (budget-aware, project-specific).
40
+ Full index at ~/.agents/skills/_index.md.
41
+ Run \`arcana curate\` to refresh after project changes.
42
+ Run \`arcana load <skill>\` for additional skills on demand.
43
+
18
44
  ## Coding Preferences
19
45
  - Follow existing patterns in the codebase
20
46
  - Handle errors explicitly
@@ -51,6 +77,11 @@ function codexTemplate(proj) {
51
77
  ## Project
52
78
  Type: ${proj.type} | Language: ${proj.lang}
53
79
 
80
+ ## Skills
81
+ Active skills curated at ~/.agents/skills/_active.md (budget-aware, project-specific).
82
+ Full index at ~/.agents/skills/_index.md.
83
+ Run \`arcana curate\` to refresh. Run \`arcana load <skill>\` for on-demand loading.
84
+
54
85
  ## Sandbox
55
86
  Codex runs in a sandboxed environment with no network access.
56
87
  All dependencies must be pre-installed before the session.
@@ -132,6 +163,7 @@ const TOOL_FILES = {
132
163
  windsurf: { path: ".windsurfrules", template: windsurfTemplate, label: "Windsurf" },
133
164
  aider: { path: ".aider.conf.yml", template: aiderTemplate, label: "Aider" },
134
165
  };
166
+ /* v8 ignore stop */
135
167
  export const SKILL_SUGGESTIONS = {
136
168
  Go: ["golang-pro", "go-linter-configuration", "testing-strategy", "security-review"],
137
169
  Rust: ["rust-best-practices", "testing-strategy", "security-review"],
@@ -146,6 +178,7 @@ export const SKILL_SUGGESTIONS_DEFAULT = [
146
178
  "codebase-dissection",
147
179
  "testing-strategy",
148
180
  ];
181
+ /* v8 ignore start */
149
182
  export async function initCommand(opts) {
150
183
  console.log(renderBanner());
151
184
  console.log();
@@ -251,5 +284,43 @@ export async function initCommand(opts) {
251
284
  const suggestions = SKILL_SUGGESTIONS[proj.type] || SKILL_SUGGESTIONS_DEFAULT;
252
285
  const skillList = suggestions.map((s) => `arcana install ${s}`).join("\n");
253
286
  p.note(skillList, "Recommended skills");
287
+ // Offer context curation
288
+ const doCurate = await p.confirm({
289
+ message: "Run context curation? (auto-selects project-relevant skills within token budget)",
290
+ initialValue: true,
291
+ });
292
+ if (!p.isCancel(doCurate) && doCurate) {
293
+ try {
294
+ const { regenerateActive } = await import("./curate.js");
295
+ const result = regenerateActive();
296
+ p.log.success(`Curated ${result.selected.length} skills (${result.totalTokens.toLocaleString()} tokens)`);
297
+ }
298
+ catch {
299
+ p.log.info("No skills installed yet. Run curate after installing skills.");
300
+ }
301
+ }
302
+ // Offer MCP server setup
303
+ const doMcp = await p.confirm({
304
+ message: "Set up MCP servers? (Context7 for live docs, etc.)",
305
+ initialValue: false,
306
+ });
307
+ if (!p.isCancel(doMcp) && doMcp) {
308
+ try {
309
+ const { installMcpServer } = await import("../mcp/install.js");
310
+ const result = installMcpServer("context7", "claude", cwd);
311
+ if (result.installed) {
312
+ p.log.success("Context7 MCP configured");
313
+ }
314
+ }
315
+ catch (err) {
316
+ p.log.warn(`MCP setup: ${err instanceof Error ? err.message : "failed"}`);
317
+ }
318
+ }
319
+ // Show detected tools
320
+ const detected = detectInstalledTools(cwd);
321
+ if (detected.length > 0) {
322
+ p.log.info(`Detected AI tools: ${detected.join(", ")}`);
323
+ }
254
324
  p.outro(`Next: ${chalk.cyan("arcana install <skill>")} or ${chalk.cyan("arcana install --all")}`);
255
325
  }
326
+ /* v8 ignore stop */
@@ -11,6 +11,7 @@ export async function installCommand(skillNames, opts) {
11
11
  if (opts.json) {
12
12
  return installJson(skillNames, opts);
13
13
  }
14
+ /* v8 ignore start */
14
15
  console.log(renderBanner());
15
16
  console.log();
16
17
  if (skillNames.length === 0 && !opts.all) {
@@ -220,6 +221,7 @@ async function installAllInteractive(providers, dryRun, force, noCheck) {
220
221
  if (failed.length > 0)
221
222
  process.exit(1);
222
223
  }
224
+ /* v8 ignore stop */
223
225
  async function installJson(skillNames, opts) {
224
226
  if (skillNames.length === 0 && !opts.all) {
225
227
  console.log(JSON.stringify({ installed: [], skipped: [], failed: [], error: "No skill specified" }));
@@ -5,6 +5,7 @@ import { isSkillInstalled, getInstallDir, readSkillMeta } from "../utils/fs.js";
5
5
  import { getProviders } from "../registry.js";
6
6
  const DESC_TRUNCATE_LENGTH = 80;
7
7
  export async function listCommand(opts) {
8
+ /* v8 ignore next */
8
9
  if (!opts.json)
9
10
  banner();
10
11
  if (opts.installed) {
@@ -16,6 +17,7 @@ export async function listCommand(opts) {
16
17
  for (const provider of providers)
17
18
  provider.clearCache();
18
19
  }
20
+ /* v8 ignore next */
19
21
  const s = opts.json ? noopSpinner() : spinner("Fetching skills...");
20
22
  s.start();
21
23
  try {
@@ -39,6 +41,7 @@ export async function listCommand(opts) {
39
41
  console.log(JSON.stringify({ skills }, null, 2));
40
42
  return;
41
43
  }
44
+ /* v8 ignore start */
42
45
  if (skills.length === 0) {
43
46
  console.log(ui.dim(" No skills found."));
44
47
  }
@@ -57,15 +60,18 @@ export async function listCommand(opts) {
57
60
  table(rows);
58
61
  }
59
62
  console.log();
63
+ /* v8 ignore stop */
60
64
  }
61
65
  catch (err) {
62
66
  if (opts.json) {
63
67
  console.log(JSON.stringify({ error: err instanceof Error ? err.message : "Failed to fetch skills" }));
64
68
  process.exit(1);
65
69
  }
70
+ /* v8 ignore start */
66
71
  s.fail("Failed to fetch skills");
67
72
  printErrorWithHint(err, true);
68
73
  process.exit(1);
74
+ /* v8 ignore stop */
69
75
  }
70
76
  }
71
77
  function listInstalled(json) {
@@ -86,6 +92,7 @@ function listInstalled(json) {
86
92
  console.log(JSON.stringify({ skills }, null, 2));
87
93
  return;
88
94
  }
95
+ /* v8 ignore start */
89
96
  if (dirs.length === 0) {
90
97
  console.log(ui.dim(" No skills installed."));
91
98
  console.log();
@@ -105,4 +112,5 @@ function listInstalled(json) {
105
112
  console.log();
106
113
  table(rows);
107
114
  console.log();
115
+ /* v8 ignore stop */
108
116
  }
@@ -0,0 +1,10 @@
1
+ /** Read full content of a skill (SKILL.md + references/ + rules/). */
2
+ export declare function readSkillContent(skillName: string): {
3
+ content: string;
4
+ files: number;
5
+ bytes: number;
6
+ } | null;
7
+ export declare function loadCommand(skillNames: string[], opts: {
8
+ json?: boolean;
9
+ append?: boolean;
10
+ }): Promise<void>;
@@ -0,0 +1,130 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getInstallDir } from "../utils/fs.js";
4
+ import { atomicWriteSync } from "../utils/atomic.js";
5
+ import { LOADED_FILENAME } from "../constants.js";
6
+ import { ui, banner } from "../utils/ui.js";
7
+ import { recordLoad } from "../utils/usage.js";
8
+ /** Read full content of a skill (SKILL.md + references/ + rules/). */
9
+ export function readSkillContent(skillName) {
10
+ const installDir = getInstallDir();
11
+ const skillDir = join(installDir, skillName);
12
+ if (!existsSync(skillDir))
13
+ return null;
14
+ const skillMd = join(skillDir, "SKILL.md");
15
+ if (!existsSync(skillMd))
16
+ return null;
17
+ const parts = [];
18
+ let totalBytes = 0;
19
+ let fileCount = 0;
20
+ // Main SKILL.md
21
+ const mainContent = readFileSync(skillMd, "utf-8");
22
+ parts.push(mainContent);
23
+ totalBytes += mainContent.length;
24
+ fileCount++;
25
+ // References directory (if exists)
26
+ const refsDir = join(skillDir, "references");
27
+ if (existsSync(refsDir)) {
28
+ try {
29
+ for (const ref of readdirSync(refsDir).sort()) {
30
+ const refPath = join(refsDir, ref);
31
+ if (!statSync(refPath).isFile())
32
+ continue;
33
+ const refContent = readFileSync(refPath, "utf-8");
34
+ parts.push(`\n---\n\n## Reference: ${ref}\n\n${refContent}`);
35
+ totalBytes += refContent.length;
36
+ fileCount++;
37
+ }
38
+ }
39
+ catch {
40
+ /* skip unreadable refs */
41
+ }
42
+ }
43
+ // Rules directory (if exists)
44
+ const rulesDir = join(skillDir, "rules");
45
+ if (existsSync(rulesDir)) {
46
+ try {
47
+ for (const rule of readdirSync(rulesDir).sort()) {
48
+ const rulePath = join(rulesDir, rule);
49
+ if (!statSync(rulePath).isFile())
50
+ continue;
51
+ const ruleContent = readFileSync(rulePath, "utf-8");
52
+ parts.push(`\n---\n\n## Rule: ${rule}\n\n${ruleContent}`);
53
+ totalBytes += ruleContent.length;
54
+ fileCount++;
55
+ }
56
+ }
57
+ catch {
58
+ /* skip unreadable rules */
59
+ }
60
+ }
61
+ return { content: parts.join("\n"), files: fileCount, bytes: totalBytes };
62
+ }
63
+ export async function loadCommand(skillNames, opts) {
64
+ if (skillNames.length === 0) {
65
+ if (opts.json) {
66
+ console.log(JSON.stringify({ error: "Specify one or more skill names" }));
67
+ }
68
+ else {
69
+ console.error("Specify one or more skill names.");
70
+ console.error("Usage: arcana load <skill> [skill2 ...]");
71
+ console.error(" arcana load golang-pro typescript --append");
72
+ }
73
+ process.exit(1);
74
+ }
75
+ const results = [];
76
+ const loadedParts = [];
77
+ for (const name of skillNames) {
78
+ const result = readSkillContent(name);
79
+ if (!result) {
80
+ results.push({ name, files: 0, bytes: 0, error: `Skill "${name}" not found` });
81
+ continue;
82
+ }
83
+ results.push({ name, files: result.files, bytes: result.bytes });
84
+ loadedParts.push(result.content);
85
+ try {
86
+ recordLoad(name);
87
+ }
88
+ catch {
89
+ /* best-effort */
90
+ }
91
+ }
92
+ if (opts.json) {
93
+ console.log(JSON.stringify({
94
+ loaded: results.filter((r) => !r.error).map((r) => ({ name: r.name, files: r.files, bytes: r.bytes })),
95
+ failed: results.filter((r) => r.error).map((r) => ({ name: r.name, error: r.error })),
96
+ }));
97
+ return;
98
+ }
99
+ if (opts.append) {
100
+ // Write aggregated content to _loaded.md
101
+ const installDir = getInstallDir();
102
+ mkdirSync(installDir, { recursive: true });
103
+ const loadedPath = join(installDir, LOADED_FILENAME);
104
+ const content = loadedParts.join("\n\n---\n\n");
105
+ atomicWriteSync(loadedPath, content, 0o644);
106
+ banner();
107
+ console.log(ui.bold(" Load Skills\n"));
108
+ for (const r of results) {
109
+ if (r.error) {
110
+ console.log(` ${ui.warn("[!!]")} ${r.name}: ${r.error}`);
111
+ }
112
+ else {
113
+ console.log(` ${ui.success("[OK]")} ${r.name} (${r.files} files, ${(r.bytes / 1024).toFixed(1)} KB)`);
114
+ }
115
+ }
116
+ console.log();
117
+ console.log(ui.dim(` Written to: ${loadedPath}`));
118
+ console.log();
119
+ }
120
+ else {
121
+ // Print to stdout for piping
122
+ for (const r of results) {
123
+ if (r.error) {
124
+ console.error(`Error: ${r.error}`);
125
+ continue;
126
+ }
127
+ }
128
+ process.stdout.write(loadedParts.join("\n\n---\n\n"));
129
+ }
130
+ }