@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/README.md +67 -0
- package/bin/cli.js +8 -0
- package/package.json +57 -0
- package/scripts/bundle-skills.mjs +34 -0
- package/scripts/clean-bundle.mjs +15 -0
- package/skills/compiler/SKILL.md +255 -0
- package/skills/compiler/references/ara-schema.md +438 -0
- package/skills/compiler/references/exploration-tree-spec.md +124 -0
- package/skills/compiler/references/validation-checklist.md +148 -0
- package/skills/research-manager/SKILL.md +588 -0
- package/skills/research-manager/references/event-taxonomy.md +160 -0
- package/skills/rigor-reviewer/SKILL.md +332 -0
- package/skills/rigor-reviewer/references/review-dimensions.md +181 -0
- package/src/agents.js +77 -0
- package/src/index.js +165 -0
- package/src/installer.js +188 -0
- package/src/prompts.js +118 -0
- package/src/skills.js +98 -0
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
|
+
}
|
package/src/installer.js
ADDED
|
@@ -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
|
+
}
|