@ara-commons/ara-skills 0.1.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.
package/src/agents.js ADDED
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ /**
6
+ * Minimal agent registry. Each entry declares where the agent expects skills
7
+ * to live, both globally (user-level) and locally (per-project).
8
+ *
9
+ * The layout we write is: <skillsDir>/<skill-id>/SKILL.md (+ any files).
10
+ */
11
+ export const SUPPORTED_AGENTS = [
12
+ {
13
+ id: 'claude-code',
14
+ name: 'Claude Code',
15
+ globalSkillsDir: path.join(os.homedir(), '.claude', 'skills'),
16
+ localSkillsDir: '.claude/skills',
17
+ detect: () => fs.existsSync(path.join(os.homedir(), '.claude')),
18
+ },
19
+ {
20
+ id: 'cursor',
21
+ name: 'Cursor',
22
+ globalSkillsDir: path.join(os.homedir(), '.cursor', 'skills'),
23
+ localSkillsDir: '.cursor/skills',
24
+ detect: () => fs.existsSync(path.join(os.homedir(), '.cursor')),
25
+ },
26
+ {
27
+ id: 'gemini-cli',
28
+ name: 'Gemini CLI',
29
+ globalSkillsDir: path.join(os.homedir(), '.gemini', 'skills'),
30
+ localSkillsDir: '.gemini/skills',
31
+ detect: () => fs.existsSync(path.join(os.homedir(), '.gemini')),
32
+ },
33
+ {
34
+ id: 'opencode',
35
+ name: 'OpenCode',
36
+ globalSkillsDir: path.join(os.homedir(), '.opencode', 'skills'),
37
+ localSkillsDir: '.opencode/skills',
38
+ detect: () => fs.existsSync(path.join(os.homedir(), '.opencode')),
39
+ },
40
+ {
41
+ id: 'codex',
42
+ name: 'Codex CLI',
43
+ globalSkillsDir: path.join(os.homedir(), '.codex', 'skills'),
44
+ localSkillsDir: '.codex/skills',
45
+ detect: () => fs.existsSync(path.join(os.homedir(), '.codex')),
46
+ },
47
+ {
48
+ id: 'hermes',
49
+ name: 'Hermes Agent',
50
+ globalSkillsDir: path.join(os.homedir(), '.hermes', 'skills'),
51
+ localSkillsDir: '.hermes/skills',
52
+ detect: () => fs.existsSync(path.join(os.homedir(), '.hermes')),
53
+ },
54
+ {
55
+ id: 'generic',
56
+ name: 'Generic (./skills)',
57
+ globalSkillsDir: path.join(os.homedir(), '.skills'),
58
+ localSkillsDir: 'skills',
59
+ detect: () => false,
60
+ },
61
+ ];
62
+
63
+ export function detectAgents() {
64
+ return SUPPORTED_AGENTS.filter((a) => a.detect());
65
+ }
66
+
67
+ export function getAgentById(id) {
68
+ return SUPPORTED_AGENTS.find((a) => a.id === id);
69
+ }
70
+
71
+ export function getSupportedAgentIds() {
72
+ return SUPPORTED_AGENTS.map((a) => a.id);
73
+ }
74
+
75
+ export function targetDirFor(agent, { local = false, cwd = process.cwd() } = {}) {
76
+ return local ? path.resolve(cwd, agent.localSkillsDir) : agent.globalSkillsDir;
77
+ }
package/src/index.js ADDED
@@ -0,0 +1,165 @@
1
+ import chalk from 'chalk';
2
+ import { SUPPORTED_AGENTS, detectAgents, getAgentById } from './agents.js';
3
+ import { listSkills } from './skills.js';
4
+ import { install, uninstall, update, listInstalled } from './installer.js';
5
+
6
+ const HELP = `
7
+ ${chalk.bold.cyan('ara-skills')} — install ARA research skills to your coding agent
8
+
9
+ ${chalk.bold('Usage:')}
10
+ npx @ara-commons/ara-skills # interactive
11
+ npx @ara-commons/ara-skills install [opts]
12
+ npx @ara-commons/ara-skills list
13
+ npx @ara-commons/ara-skills update [opts]
14
+ npx @ara-commons/ara-skills uninstall [opts]
15
+ npx @ara-commons/ara-skills skills # show available skills
16
+ npx @ara-commons/ara-skills agents # show supported agents
17
+
18
+ ${chalk.bold('Install options:')}
19
+ --all Install every skill (default if no --skill given)
20
+ --skill <id> Install one skill (repeatable). Ids: compiler, research-manager, rigor-reviewer
21
+ --agent <id> Target one agent (repeatable). Default: auto-detect, else claude-code
22
+ --local Install into ./<agent>/skills instead of $HOME
23
+ --force Overwrite existing installations
24
+ --quiet Suppress per-skill log output
25
+
26
+ ${chalk.bold('Examples:')}
27
+ npx @ara-commons/ara-skills install --all
28
+ npx @ara-commons/ara-skills install --skill compiler --agent claude-code
29
+ npx @ara-commons/ara-skills install --all --local
30
+ npx @ara-commons/ara-skills uninstall --skill rigor-reviewer --agent cursor
31
+ `;
32
+
33
+ function parseArgs(argv) {
34
+ const out = { _: [], skills: [], agents: [], flags: {} };
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const a = argv[i];
37
+ if (a === '--skill' || a === '-s') out.skills.push(argv[++i]);
38
+ else if (a === '--agent' || a === '-a') out.agents.push(argv[++i]);
39
+ else if (a === '--all') out.flags.all = true;
40
+ else if (a === '--local') out.flags.local = true;
41
+ else if (a === '--force') out.flags.force = true;
42
+ else if (a === '--quiet' || a === '-q') out.flags.quiet = true;
43
+ else if (a === '--help' || a === '-h') out.flags.help = true;
44
+ else if (a === '--version' || a === '-v') out.flags.version = true;
45
+ else if (a.startsWith('-')) throw new Error(`Unknown flag: ${a}`);
46
+ else out._.push(a);
47
+ }
48
+ return out;
49
+ }
50
+
51
+ function resolveAgents(requested) {
52
+ if (requested.length > 0) {
53
+ for (const id of requested) {
54
+ if (!getAgentById(id)) {
55
+ throw new Error(
56
+ `Unknown agent "${id}". Supported: ${SUPPORTED_AGENTS.map((a) => a.id).join(', ')}`
57
+ );
58
+ }
59
+ }
60
+ return requested;
61
+ }
62
+ const detected = detectAgents();
63
+ if (detected.length > 0) return detected.map((a) => a.id);
64
+ return ['claude-code'];
65
+ }
66
+
67
+ export async function main(argv) {
68
+ const args = parseArgs(argv);
69
+
70
+ if (args.flags.help) {
71
+ console.log(HELP);
72
+ return;
73
+ }
74
+ if (args.flags.version) {
75
+ const { readFileSync } = await import('node:fs');
76
+ const { fileURLToPath } = await import('node:url');
77
+ const { dirname, join } = await import('node:path');
78
+ const here = dirname(fileURLToPath(import.meta.url));
79
+ const pkg = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8'));
80
+ console.log(pkg.version);
81
+ return;
82
+ }
83
+
84
+ const cmd = args._[0];
85
+
86
+ if (!cmd) {
87
+ const { interactiveFlow } = await import('./prompts.js');
88
+ await interactiveFlow();
89
+ return;
90
+ }
91
+
92
+ if (cmd === 'skills') {
93
+ for (const s of listSkills()) {
94
+ console.log(`${chalk.bold(s.id)}`);
95
+ console.log(` ${chalk.gray(s.description.slice(0, 120))}`);
96
+ }
97
+ return;
98
+ }
99
+
100
+ if (cmd === 'agents') {
101
+ const detected = new Set(detectAgents().map((a) => a.id));
102
+ for (const a of SUPPORTED_AGENTS) {
103
+ const tag = detected.has(a.id) ? chalk.green('✓ detected') : chalk.gray('not detected');
104
+ console.log(`${chalk.bold(a.id.padEnd(14))} ${tag} ${chalk.gray(a.globalSkillsDir)}`);
105
+ }
106
+ return;
107
+ }
108
+
109
+ if (cmd === 'list') {
110
+ const rows = listInstalled();
111
+ if (rows.length === 0) {
112
+ console.log('No ARA skills installed.');
113
+ return;
114
+ }
115
+ for (const r of rows) {
116
+ console.log(`${chalk.bold(r.agent)} (${r.scope}) ${chalk.gray(r.dir)}`);
117
+ for (const s of r.skills) console.log(` • ${s}`);
118
+ }
119
+ return;
120
+ }
121
+
122
+ const local = !!args.flags.local;
123
+ const force = !!args.flags.force;
124
+ const quiet = !!args.flags.quiet;
125
+
126
+ if (cmd === 'install') {
127
+ const agentIds = resolveAgents(args.agents);
128
+ const skillIds = args.skills;
129
+ if (!quiet) {
130
+ const label = skillIds.length ? skillIds.join(', ') : 'all skills';
131
+ console.log(
132
+ `Installing ${chalk.bold(label)} to ${chalk.bold(agentIds.join(', '))} (${
133
+ local ? 'local' : 'global'
134
+ })`
135
+ );
136
+ }
137
+ for (const agentId of agentIds) {
138
+ if (!quiet) console.log(chalk.bold(`→ ${agentId}`));
139
+ install({ agentId, skillIds, local, force, quiet });
140
+ }
141
+ return;
142
+ }
143
+
144
+ if (cmd === 'update') {
145
+ const agentIds = resolveAgents(args.agents);
146
+ for (const agentId of agentIds) {
147
+ if (!quiet) console.log(chalk.bold(`→ ${agentId}`));
148
+ update({ agentId, local, quiet });
149
+ }
150
+ return;
151
+ }
152
+
153
+ if (cmd === 'uninstall') {
154
+ const agentIds = resolveAgents(args.agents);
155
+ const skillIds = args.skills;
156
+ for (const agentId of agentIds) {
157
+ if (!quiet) console.log(chalk.bold(`→ ${agentId}`));
158
+ uninstall({ agentId, skillIds, local, quiet });
159
+ }
160
+ return;
161
+ }
162
+
163
+ console.log(HELP);
164
+ throw new Error(`Unknown command: ${cmd}`);
165
+ }
@@ -0,0 +1,188 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { listSkills, getSkill } from './skills.js';
4
+ import { SUPPORTED_AGENTS, getAgentById, targetDirFor } from './agents.js';
5
+
6
+ const LOCK_FILE = '.ara-skills.json';
7
+
8
+ function ensureDir(dir) {
9
+ fs.mkdirSync(dir, { recursive: true });
10
+ }
11
+
12
+ function copyDir(src, dst) {
13
+ fs.mkdirSync(dst, { recursive: true });
14
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
15
+ const s = path.join(src, entry.name);
16
+ const d = path.join(dst, entry.name);
17
+ if (entry.isDirectory()) copyDir(s, d);
18
+ else if (entry.isSymbolicLink()) {
19
+ const link = fs.readlinkSync(s);
20
+ try {
21
+ fs.symlinkSync(link, d);
22
+ } catch {
23
+ fs.copyFileSync(s, d);
24
+ }
25
+ } else fs.copyFileSync(s, d);
26
+ }
27
+ }
28
+
29
+ function rmIfExists(p) {
30
+ if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
31
+ }
32
+
33
+ function readLock(dir) {
34
+ const file = path.join(dir, LOCK_FILE);
35
+ if (!fs.existsSync(file)) return { skills: {} };
36
+ try {
37
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
38
+ } catch {
39
+ return { skills: {} };
40
+ }
41
+ }
42
+
43
+ function writeLock(dir, data) {
44
+ const file = path.join(dir, LOCK_FILE);
45
+ ensureDir(dir);
46
+ fs.writeFileSync(
47
+ file,
48
+ JSON.stringify({ updatedAt: new Date().toISOString(), ...data }, null, 2)
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Install a set of skills to a single agent's skills directory.
54
+ *
55
+ * opts:
56
+ * agentId: string, required
57
+ * skillIds: string[] — empty array means "all skills"
58
+ * local: boolean — install into the current project rather than $HOME
59
+ * cwd: string — overrides process.cwd() for local installs
60
+ * force: boolean — overwrite existing installations
61
+ * quiet: boolean — suppress log output
62
+ */
63
+ export function install(opts) {
64
+ const { agentId, skillIds = [], local = false, cwd, force = false, quiet = false } = opts;
65
+ const agent = getAgentById(agentId);
66
+ if (!agent) throw new Error(`Unknown agent: ${agentId}`);
67
+
68
+ const allSkills = listSkills();
69
+ const selected =
70
+ skillIds.length === 0
71
+ ? allSkills
72
+ : skillIds.map((id) => {
73
+ const s = getSkill(id);
74
+ if (!s) throw new Error(`Unknown skill: ${id}`);
75
+ return s;
76
+ });
77
+
78
+ const targetDir = targetDirFor(agent, { local, cwd });
79
+ ensureDir(targetDir);
80
+
81
+ const results = [];
82
+ const lock = readLock(targetDir);
83
+ lock.skills = lock.skills || {};
84
+
85
+ for (const skill of selected) {
86
+ const dest = path.join(targetDir, skill.id);
87
+ const exists = fs.existsSync(dest);
88
+ if (exists && !force) {
89
+ results.push({ skill: skill.id, status: 'skipped', reason: 'already installed' });
90
+ if (!quiet) console.log(` ~ ${skill.id} already installed (use --force to overwrite)`);
91
+ continue;
92
+ }
93
+ if (exists) rmIfExists(dest);
94
+ copyDir(skill.path, dest);
95
+ lock.skills[skill.id] = {
96
+ installedAt: new Date().toISOString(),
97
+ source: '@ara-commons/ara-skills',
98
+ };
99
+ results.push({ skill: skill.id, status: 'installed', dest });
100
+ if (!quiet) console.log(` + ${skill.id} -> ${dest}`);
101
+ }
102
+
103
+ writeLock(targetDir, lock);
104
+ return { agent: agent.id, targetDir, results };
105
+ }
106
+
107
+ /**
108
+ * Install to multiple agents at once.
109
+ */
110
+ export function installMany(opts) {
111
+ const { agentIds, ...rest } = opts;
112
+ return agentIds.map((agentId) => install({ ...rest, agentId }));
113
+ }
114
+
115
+ export function uninstall(opts) {
116
+ const { agentId, skillIds = [], local = false, cwd, quiet = false } = opts;
117
+ const agent = getAgentById(agentId);
118
+ if (!agent) throw new Error(`Unknown agent: ${agentId}`);
119
+
120
+ const targetDir = targetDirFor(agent, { local, cwd });
121
+ if (!fs.existsSync(targetDir)) {
122
+ return { agent: agent.id, targetDir, results: [] };
123
+ }
124
+
125
+ const lock = readLock(targetDir);
126
+ const known = Object.keys(lock.skills || {});
127
+ const targets = skillIds.length === 0 ? known : skillIds;
128
+
129
+ const results = [];
130
+ for (const id of targets) {
131
+ const dest = path.join(targetDir, id);
132
+ if (fs.existsSync(dest)) {
133
+ rmIfExists(dest);
134
+ delete lock.skills[id];
135
+ results.push({ skill: id, status: 'removed' });
136
+ if (!quiet) console.log(` - ${id}`);
137
+ } else {
138
+ results.push({ skill: id, status: 'missing' });
139
+ }
140
+ }
141
+ writeLock(targetDir, lock);
142
+ return { agent: agent.id, targetDir, results };
143
+ }
144
+
145
+ /**
146
+ * "Update" = re-install (overwrite) every currently tracked skill.
147
+ */
148
+ export function update(opts) {
149
+ const { agentId, local = false, cwd, quiet = false } = opts;
150
+ const agent = getAgentById(agentId);
151
+ if (!agent) throw new Error(`Unknown agent: ${agentId}`);
152
+
153
+ const targetDir = targetDirFor(agent, { local, cwd });
154
+ const lock = readLock(targetDir);
155
+ const ids = Object.keys(lock.skills || {});
156
+ if (ids.length === 0) {
157
+ if (!quiet) console.log(` (no skills tracked at ${targetDir})`);
158
+ return { agent: agent.id, targetDir, results: [] };
159
+ }
160
+ return install({ agentId, skillIds: ids, local, cwd, force: true, quiet });
161
+ }
162
+
163
+ /**
164
+ * List currently installed skills across all known agents.
165
+ * Checks both global and local (cwd) locations.
166
+ */
167
+ export function listInstalled({ cwd = process.cwd() } = {}) {
168
+ const rows = [];
169
+ for (const agent of SUPPORTED_AGENTS) {
170
+ for (const scope of ['global', 'local']) {
171
+ const dir = targetDirFor(agent, { local: scope === 'local', cwd });
172
+ if (!fs.existsSync(dir)) continue;
173
+ const lock = readLock(dir);
174
+ const skills = Object.keys(lock.skills || {});
175
+ if (skills.length === 0) {
176
+ // Also look for skill dirs present without a lock file.
177
+ const bare = fs
178
+ .readdirSync(dir, { withFileTypes: true })
179
+ .filter((e) => e.isDirectory() && fs.existsSync(path.join(dir, e.name, 'SKILL.md')))
180
+ .map((e) => e.name);
181
+ if (bare.length > 0) rows.push({ agent: agent.id, scope, dir, skills: bare, tracked: false });
182
+ } else {
183
+ rows.push({ agent: agent.id, scope, dir, skills, tracked: true });
184
+ }
185
+ }
186
+ }
187
+ return rows;
188
+ }
package/src/prompts.js ADDED
@@ -0,0 +1,118 @@
1
+ import { checkbox, select, confirm } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import { SUPPORTED_AGENTS, detectAgents } from './agents.js';
4
+ import { listSkills } from './skills.js';
5
+ import { install, uninstall, update, listInstalled } from './installer.js';
6
+
7
+ function banner() {
8
+ console.log();
9
+ console.log(chalk.bold.cyan(' ARA Skills'));
10
+ console.log(chalk.gray(' Agent-Native Research Artifact — skills installer'));
11
+ console.log();
12
+ }
13
+
14
+ export async function interactiveFlow() {
15
+ banner();
16
+
17
+ const skills = listSkills();
18
+ const detected = detectAgents();
19
+ if (detected.length > 0) {
20
+ console.log(chalk.gray(` Detected agents: ${detected.map((a) => a.name).join(', ')}`));
21
+ } else {
22
+ console.log(chalk.yellow(' No coding agents detected — you can still install locally.'));
23
+ }
24
+ console.log();
25
+
26
+ const action = await select({
27
+ message: 'What would you like to do?',
28
+ choices: [
29
+ { name: 'Install skills', value: 'install' },
30
+ { name: 'List installed skills', value: 'list' },
31
+ { name: 'Update installed skills', value: 'update' },
32
+ { name: 'Uninstall skills', value: 'uninstall' },
33
+ { name: 'Exit', value: 'exit' },
34
+ ],
35
+ });
36
+
37
+ if (action === 'exit') return;
38
+
39
+ if (action === 'list') {
40
+ const rows = listInstalled();
41
+ if (rows.length === 0) {
42
+ console.log(chalk.gray(' No ARA skills installed.'));
43
+ return;
44
+ }
45
+ for (const r of rows) {
46
+ console.log(` ${chalk.bold(r.agent)} (${r.scope}) — ${chalk.gray(r.dir)}`);
47
+ for (const s of r.skills) console.log(` • ${s}`);
48
+ }
49
+ return;
50
+ }
51
+
52
+ const scope = await select({
53
+ message: 'Install scope',
54
+ choices: [
55
+ { name: 'Global (user-level, available in every project)', value: 'global' },
56
+ { name: 'Local (just this project / current directory)', value: 'local' },
57
+ ],
58
+ });
59
+ const local = scope === 'local';
60
+
61
+ const agentChoices = SUPPORTED_AGENTS.map((a) => ({
62
+ name: `${a.name}${detected.includes(a) ? chalk.green(' ✓ detected') : ''}`,
63
+ value: a.id,
64
+ checked: detected.includes(a),
65
+ }));
66
+ const agentIds = await checkbox({
67
+ message: 'Pick target agents',
68
+ choices: agentChoices,
69
+ required: true,
70
+ });
71
+
72
+ if (action === 'install') {
73
+ const skillIds = await checkbox({
74
+ message: 'Pick skills to install',
75
+ choices: skills.map((s) => ({
76
+ name: `${chalk.bold(s.id)} — ${chalk.gray(s.description.slice(0, 80))}`,
77
+ value: s.id,
78
+ checked: true,
79
+ })),
80
+ required: true,
81
+ });
82
+ const force = await confirm({
83
+ message: 'Overwrite existing installations if present?',
84
+ default: true,
85
+ });
86
+ console.log();
87
+ for (const agentId of agentIds) {
88
+ console.log(chalk.bold(`→ ${agentId} (${local ? 'local' : 'global'})`));
89
+ install({ agentId, skillIds, local, force });
90
+ }
91
+ }
92
+
93
+ if (action === 'update') {
94
+ console.log();
95
+ for (const agentId of agentIds) {
96
+ console.log(chalk.bold(`→ ${agentId} (${local ? 'local' : 'global'})`));
97
+ update({ agentId, local });
98
+ }
99
+ }
100
+
101
+ if (action === 'uninstall') {
102
+ const skillIds = await checkbox({
103
+ message: 'Pick skills to remove (empty = all tracked)',
104
+ choices: skills.map((s) => ({
105
+ name: `${chalk.bold(s.id)}`,
106
+ value: s.id,
107
+ })),
108
+ });
109
+ console.log();
110
+ for (const agentId of agentIds) {
111
+ console.log(chalk.bold(`→ ${agentId} (${local ? 'local' : 'global'})`));
112
+ uninstall({ agentId, skillIds, local });
113
+ }
114
+ }
115
+
116
+ console.log();
117
+ console.log(chalk.green(' Done.'));
118
+ }
package/src/skills.js ADDED
@@ -0,0 +1,98 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
7
+
8
+ /**
9
+ * Locate the directory that holds the skill sources.
10
+ *
11
+ * Two modes:
12
+ * 1. Published package: skills are bundled at <pkg>/skills/ (via `prepack`).
13
+ * 2. Dev mode (running from monorepo): fall back to <repoRoot>/skills/
14
+ * which is <pkg>/../../skills.
15
+ */
16
+ export function resolveSkillsRoot() {
17
+ const bundled = path.join(PACKAGE_ROOT, 'skills');
18
+ if (isSkillsDir(bundled)) return bundled;
19
+
20
+ const monorepo = path.resolve(PACKAGE_ROOT, '..', '..', 'skills');
21
+ if (isSkillsDir(monorepo)) return monorepo;
22
+
23
+ throw new Error(
24
+ `Could not locate skills directory. Looked in:\n ${bundled}\n ${monorepo}`
25
+ );
26
+ }
27
+
28
+ function isSkillsDir(dir) {
29
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return false;
30
+ const entries = fs.readdirSync(dir);
31
+ return entries.some((name) => {
32
+ const skillMd = path.join(dir, name, 'SKILL.md');
33
+ return fs.existsSync(skillMd);
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Parse the YAML frontmatter at the top of SKILL.md.
39
+ * Intentionally minimal — we only need `name` and `description`.
40
+ */
41
+ function parseFrontmatter(mdPath) {
42
+ const src = fs.readFileSync(mdPath, 'utf8');
43
+ if (!src.startsWith('---')) return {};
44
+ const end = src.indexOf('\n---', 3);
45
+ if (end < 0) return {};
46
+ const body = src.slice(3, end).trim();
47
+
48
+ const out = {};
49
+ let currentKey = null;
50
+ let buffer = [];
51
+ const flush = () => {
52
+ if (currentKey !== null) {
53
+ out[currentKey] = buffer.join('\n').trim();
54
+ }
55
+ };
56
+
57
+ for (const raw of body.split('\n')) {
58
+ const line = raw.replace(/\r$/, '');
59
+ const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
60
+ // A new top-level key starts only when the line has no leading whitespace.
61
+ if (m && !raw.startsWith(' ') && !raw.startsWith('\t')) {
62
+ flush();
63
+ currentKey = m[1];
64
+ const value = m[2];
65
+ buffer = value === '' || value === '|' || value === '>' ? [] : [value];
66
+ } else if (currentKey) {
67
+ buffer.push(line.replace(/^\s{2}/, ''));
68
+ }
69
+ }
70
+ flush();
71
+ return out;
72
+ }
73
+
74
+ /**
75
+ * Discover all skills by scanning <root>/<id>/SKILL.md.
76
+ * Returns: [{ id, name, description, path }]
77
+ */
78
+ export function listSkills(root = resolveSkillsRoot()) {
79
+ const out = [];
80
+ for (const id of fs.readdirSync(root).sort()) {
81
+ const skillDir = path.join(root, id);
82
+ const skillMd = path.join(skillDir, 'SKILL.md');
83
+ if (!fs.existsSync(skillMd)) continue;
84
+ if (!fs.statSync(skillDir).isDirectory()) continue;
85
+
86
+ const meta = parseFrontmatter(skillMd);
87
+ const name = meta.name || id;
88
+ const description =
89
+ (meta.description || '').replace(/\s+/g, ' ').trim().slice(0, 240) ||
90
+ `(no description)`;
91
+ out.push({ id, name, description, path: skillDir });
92
+ }
93
+ return out;
94
+ }
95
+
96
+ export function getSkill(id) {
97
+ return listSkills().find((s) => s.id === id || s.name === id);
98
+ }