@orchestra-research/ai-research-skills 1.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.
- package/README.md +97 -0
- package/bin/cli.js +8 -0
- package/package.json +46 -0
- package/src/agents.js +81 -0
- package/src/ascii.js +126 -0
- package/src/index.js +282 -0
- package/src/installer.js +391 -0
- package/src/prompts.js +436 -0
package/src/installer.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, symlinkSync, readdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join, basename, dirname } from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
|
|
8
|
+
const REPO_URL = 'https://github.com/zechenzhangAGI/AI-research-SKILLs';
|
|
9
|
+
const CANONICAL_DIR = join(homedir(), '.orchestra', 'skills');
|
|
10
|
+
const LOCK_FILE = join(homedir(), '.orchestra', '.lock.json');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ensure the canonical skills directory exists
|
|
14
|
+
*/
|
|
15
|
+
function ensureCanonicalDir() {
|
|
16
|
+
const orchestraDir = join(homedir(), '.orchestra');
|
|
17
|
+
if (!existsSync(orchestraDir)) {
|
|
18
|
+
mkdirSync(orchestraDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
if (!existsSync(CANONICAL_DIR)) {
|
|
21
|
+
mkdirSync(CANONICAL_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read lock file
|
|
27
|
+
*/
|
|
28
|
+
function readLock() {
|
|
29
|
+
if (existsSync(LOCK_FILE)) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(LOCK_FILE, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return { version: null, installedAt: null, skills: [] };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { version: null, installedAt: null, skills: [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Write lock file
|
|
41
|
+
*/
|
|
42
|
+
function writeLock(data) {
|
|
43
|
+
writeFileSync(LOCK_FILE, JSON.stringify(data, null, 2));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Download skills from GitHub
|
|
48
|
+
*/
|
|
49
|
+
async function downloadSkills(categories, spinner) {
|
|
50
|
+
ensureCanonicalDir();
|
|
51
|
+
|
|
52
|
+
// Clone or update the repository to a temp location
|
|
53
|
+
const tempDir = join(homedir(), '.orchestra', '.temp-clone');
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (existsSync(tempDir)) {
|
|
57
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
spinner.text = 'Cloning repository...';
|
|
61
|
+
execSync(`git clone --depth 1 ${REPO_URL}.git ${tempDir}`, {
|
|
62
|
+
stdio: 'pipe',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const skills = [];
|
|
66
|
+
|
|
67
|
+
// Copy selected categories
|
|
68
|
+
for (const categoryId of categories) {
|
|
69
|
+
const categoryPath = join(tempDir, categoryId);
|
|
70
|
+
if (!existsSync(categoryPath)) continue;
|
|
71
|
+
|
|
72
|
+
const targetCategoryPath = join(CANONICAL_DIR, categoryId);
|
|
73
|
+
if (!existsSync(targetCategoryPath)) {
|
|
74
|
+
mkdirSync(targetCategoryPath, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if it's a standalone skill (SKILL.md directly in category)
|
|
78
|
+
const standaloneSkillPath = join(categoryPath, 'SKILL.md');
|
|
79
|
+
if (existsSync(standaloneSkillPath)) {
|
|
80
|
+
// Copy the entire category as a standalone skill
|
|
81
|
+
spinner.text = `Downloading ${categoryId}...`;
|
|
82
|
+
execSync(`cp -r "${categoryPath}"/* "${targetCategoryPath}/"`, { stdio: 'pipe' });
|
|
83
|
+
skills.push({ category: categoryId, skill: categoryId, standalone: true });
|
|
84
|
+
} else {
|
|
85
|
+
// It's a nested category with multiple skills
|
|
86
|
+
const entries = readdirSync(categoryPath, { withFileTypes: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
const skillPath = join(categoryPath, entry.name, 'SKILL.md');
|
|
90
|
+
if (existsSync(skillPath)) {
|
|
91
|
+
spinner.text = `Downloading ${entry.name}...`;
|
|
92
|
+
const targetSkillPath = join(targetCategoryPath, entry.name);
|
|
93
|
+
if (!existsSync(targetSkillPath)) {
|
|
94
|
+
mkdirSync(targetSkillPath, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
execSync(`cp -r "${join(categoryPath, entry.name)}"/* "${targetSkillPath}/"`, { stdio: 'pipe' });
|
|
97
|
+
skills.push({ category: categoryId, skill: entry.name, standalone: false });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Cleanup
|
|
105
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
106
|
+
|
|
107
|
+
return skills;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
if (existsSync(tempDir)) {
|
|
110
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create symlinks for an agent
|
|
118
|
+
*/
|
|
119
|
+
function createSymlinks(agent, skills, spinner) {
|
|
120
|
+
const agentSkillsPath = agent.skillsPath;
|
|
121
|
+
|
|
122
|
+
// Ensure agent skills directory exists
|
|
123
|
+
if (!existsSync(agentSkillsPath)) {
|
|
124
|
+
mkdirSync(agentSkillsPath, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let linkedCount = 0;
|
|
128
|
+
|
|
129
|
+
for (const skill of skills) {
|
|
130
|
+
const sourcePath = skill.standalone
|
|
131
|
+
? join(CANONICAL_DIR, skill.category)
|
|
132
|
+
: join(CANONICAL_DIR, skill.category, skill.skill);
|
|
133
|
+
|
|
134
|
+
const linkName = skill.standalone ? skill.category : skill.skill;
|
|
135
|
+
const linkPath = join(agentSkillsPath, linkName);
|
|
136
|
+
|
|
137
|
+
// Remove existing symlink if present
|
|
138
|
+
if (existsSync(linkPath)) {
|
|
139
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
symlinkSync(sourcePath, linkPath);
|
|
144
|
+
linkedCount++;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
// Ignore symlink errors (e.g., already exists)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return linkedCount;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Download specific skills from GitHub
|
|
155
|
+
*/
|
|
156
|
+
async function downloadSpecificSkills(skillPaths, spinner) {
|
|
157
|
+
ensureCanonicalDir();
|
|
158
|
+
|
|
159
|
+
// Clone or update the repository to a temp location
|
|
160
|
+
const tempDir = join(homedir(), '.orchestra', '.temp-clone');
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
if (existsSync(tempDir)) {
|
|
164
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
spinner.text = 'Cloning repository...';
|
|
168
|
+
execSync(`git clone --depth 1 ${REPO_URL}.git ${tempDir}`, {
|
|
169
|
+
stdio: 'pipe',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const skills = [];
|
|
173
|
+
|
|
174
|
+
// Copy selected skills
|
|
175
|
+
for (const skillPath of skillPaths) {
|
|
176
|
+
// skillPath can be like '06-post-training/verl' or '20-ml-paper-writing' (standalone)
|
|
177
|
+
const parts = skillPath.split('/');
|
|
178
|
+
const categoryId = parts[0];
|
|
179
|
+
const skillName = parts[1] || null;
|
|
180
|
+
|
|
181
|
+
const targetCategoryPath = join(CANONICAL_DIR, categoryId);
|
|
182
|
+
if (!existsSync(targetCategoryPath)) {
|
|
183
|
+
mkdirSync(targetCategoryPath, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (skillName) {
|
|
187
|
+
// Nested skill like '06-post-training/verl'
|
|
188
|
+
const sourcePath = join(tempDir, categoryId, skillName);
|
|
189
|
+
if (existsSync(sourcePath)) {
|
|
190
|
+
spinner.text = `Downloading ${skillName}...`;
|
|
191
|
+
const targetSkillPath = join(targetCategoryPath, skillName);
|
|
192
|
+
if (!existsSync(targetSkillPath)) {
|
|
193
|
+
mkdirSync(targetSkillPath, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
execSync(`cp -r "${sourcePath}"/* "${targetSkillPath}/"`, { stdio: 'pipe' });
|
|
196
|
+
skills.push({ category: categoryId, skill: skillName, standalone: false });
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// Standalone skill like '20-ml-paper-writing'
|
|
200
|
+
const sourcePath = join(tempDir, categoryId);
|
|
201
|
+
if (existsSync(sourcePath)) {
|
|
202
|
+
spinner.text = `Downloading ${categoryId}...`;
|
|
203
|
+
execSync(`cp -r "${sourcePath}"/* "${targetCategoryPath}/"`, { stdio: 'pipe' });
|
|
204
|
+
skills.push({ category: categoryId, skill: categoryId, standalone: true });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Cleanup
|
|
210
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
211
|
+
|
|
212
|
+
return skills;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (existsSync(tempDir)) {
|
|
215
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Install specific skills to agents
|
|
223
|
+
*/
|
|
224
|
+
export async function installSpecificSkills(skillPaths, agents) {
|
|
225
|
+
const spinner = ora('Downloading from GitHub...').start();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
// Download skills
|
|
229
|
+
const skills = await downloadSpecificSkills(skillPaths, spinner);
|
|
230
|
+
spinner.succeed(`Downloaded ${skills.length} skills`);
|
|
231
|
+
|
|
232
|
+
// Create symlinks for each agent
|
|
233
|
+
spinner.start('Creating symlinks...');
|
|
234
|
+
|
|
235
|
+
for (const agent of agents) {
|
|
236
|
+
const count = createSymlinks(agent, skills, spinner);
|
|
237
|
+
console.log(` ${chalk.green('✓')} ${agent.name.padEnd(14)} ${chalk.dim('→')} ${agent.skillsPath.replace(homedir(), '~').padEnd(25)} ${chalk.green(count + ' skills')}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
spinner.stop();
|
|
241
|
+
|
|
242
|
+
// Update lock file
|
|
243
|
+
const lock = readLock();
|
|
244
|
+
lock.version = '1.0.0';
|
|
245
|
+
lock.installedAt = new Date().toISOString();
|
|
246
|
+
lock.skills = [...(lock.skills || []), ...skills];
|
|
247
|
+
lock.agents = agents.map(a => a.id);
|
|
248
|
+
writeLock(lock);
|
|
249
|
+
|
|
250
|
+
return skills.length;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
spinner.fail('Installation failed');
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Install skills to agents
|
|
259
|
+
*/
|
|
260
|
+
export async function installSkills(categories, agents) {
|
|
261
|
+
const spinner = ora('Downloading from GitHub...').start();
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// Download skills
|
|
265
|
+
const skills = await downloadSkills(categories, spinner);
|
|
266
|
+
spinner.succeed(`Downloaded ${skills.length} skills`);
|
|
267
|
+
|
|
268
|
+
// Create symlinks for each agent
|
|
269
|
+
spinner.start('Creating symlinks...');
|
|
270
|
+
const results = [];
|
|
271
|
+
|
|
272
|
+
for (const agent of agents) {
|
|
273
|
+
const count = createSymlinks(agent, skills, spinner);
|
|
274
|
+
results.push({ agent, count });
|
|
275
|
+
console.log(` ${chalk.green('✓')} ${agent.name.padEnd(14)} ${chalk.dim('→')} ${agent.skillsPath.replace(homedir(), '~').padEnd(25)} ${chalk.green(count + ' skills')}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
spinner.stop();
|
|
279
|
+
|
|
280
|
+
// Update lock file
|
|
281
|
+
const lock = readLock();
|
|
282
|
+
lock.version = '1.0.0';
|
|
283
|
+
lock.installedAt = new Date().toISOString();
|
|
284
|
+
lock.skills = skills;
|
|
285
|
+
lock.agents = agents.map(a => a.id);
|
|
286
|
+
writeLock(lock);
|
|
287
|
+
|
|
288
|
+
return skills.length;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
spinner.fail('Installation failed');
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* List installed skills by scanning actual folders
|
|
297
|
+
*/
|
|
298
|
+
export function listInstalledSkills() {
|
|
299
|
+
// Check if canonical skills directory exists
|
|
300
|
+
if (!existsSync(CANONICAL_DIR)) {
|
|
301
|
+
console.log(chalk.yellow(' No skills installed yet.'));
|
|
302
|
+
console.log();
|
|
303
|
+
console.log(` Run ${chalk.cyan('npx @orchestra-research/ai-research-skills')} to install skills.`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Scan the actual skills directory
|
|
308
|
+
const categories = readdirSync(CANONICAL_DIR, { withFileTypes: true })
|
|
309
|
+
.filter(d => d.isDirectory())
|
|
310
|
+
.map(d => d.name)
|
|
311
|
+
.sort();
|
|
312
|
+
|
|
313
|
+
if (categories.length === 0) {
|
|
314
|
+
console.log(chalk.yellow(' No skills installed yet.'));
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(` Run ${chalk.cyan('npx @orchestra-research/ai-research-skills')} to install skills.`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const byCategory = {};
|
|
321
|
+
let totalSkills = 0;
|
|
322
|
+
|
|
323
|
+
for (const category of categories) {
|
|
324
|
+
const categoryPath = join(CANONICAL_DIR, category);
|
|
325
|
+
|
|
326
|
+
// Check if it's a standalone skill (has SKILL.md directly)
|
|
327
|
+
const standaloneSkill = join(categoryPath, 'SKILL.md');
|
|
328
|
+
if (existsSync(standaloneSkill)) {
|
|
329
|
+
byCategory[category] = [category];
|
|
330
|
+
totalSkills++;
|
|
331
|
+
} else {
|
|
332
|
+
// It's a category with nested skills
|
|
333
|
+
const skills = readdirSync(categoryPath, { withFileTypes: true })
|
|
334
|
+
.filter(d => d.isDirectory() && existsSync(join(categoryPath, d.name, 'SKILL.md')))
|
|
335
|
+
.map(d => d.name)
|
|
336
|
+
.sort();
|
|
337
|
+
|
|
338
|
+
if (skills.length > 0) {
|
|
339
|
+
byCategory[category] = skills;
|
|
340
|
+
totalSkills += skills.length;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(chalk.white.bold(` Installed Skills (${totalSkills})`));
|
|
346
|
+
console.log();
|
|
347
|
+
|
|
348
|
+
for (const [category, skills] of Object.entries(byCategory)) {
|
|
349
|
+
console.log(chalk.cyan(` ${category}`));
|
|
350
|
+
for (const skill of skills) {
|
|
351
|
+
if (skill === category) {
|
|
352
|
+
// Standalone skill
|
|
353
|
+
console.log(` ${chalk.dim('●')} ${chalk.white('(standalone)')}`);
|
|
354
|
+
} else {
|
|
355
|
+
console.log(` ${chalk.dim('●')} ${skill}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
console.log();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Show storage location
|
|
362
|
+
console.log(chalk.dim(` Location: ${CANONICAL_DIR.replace(homedir(), '~')}`));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get all category IDs
|
|
367
|
+
*/
|
|
368
|
+
export function getAllCategoryIds() {
|
|
369
|
+
return [
|
|
370
|
+
'01-model-architecture',
|
|
371
|
+
'02-tokenization',
|
|
372
|
+
'03-fine-tuning',
|
|
373
|
+
'04-mechanistic-interpretability',
|
|
374
|
+
'05-data-processing',
|
|
375
|
+
'06-post-training',
|
|
376
|
+
'07-safety-alignment',
|
|
377
|
+
'08-distributed-training',
|
|
378
|
+
'09-infrastructure',
|
|
379
|
+
'10-optimization',
|
|
380
|
+
'11-evaluation',
|
|
381
|
+
'12-inference-serving',
|
|
382
|
+
'13-mlops',
|
|
383
|
+
'14-agents',
|
|
384
|
+
'15-rag',
|
|
385
|
+
'16-prompt-engineering',
|
|
386
|
+
'17-observability',
|
|
387
|
+
'18-multimodal',
|
|
388
|
+
'19-emerging-techniques',
|
|
389
|
+
'20-ml-paper-writing',
|
|
390
|
+
];
|
|
391
|
+
}
|