@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.
@@ -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
+ }