@orchestra-research/ai-research-skills 1.0.0 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchestra-research/ai-research-skills",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Install AI research engineering skills to your coding agents (Claude Code, Cursor, Gemini CLI, and more)",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -11,13 +11,16 @@ import {
11
11
  askMainMenuAction,
12
12
  askSelectAgents,
13
13
  askAfterAction,
14
+ askUninstallChoice,
15
+ askSelectSkillsToUninstall,
16
+ askConfirmUninstall,
14
17
  parseArgs,
15
18
  CATEGORIES,
16
19
  INDIVIDUAL_SKILLS,
17
20
  QUICK_START_SKILLS,
18
21
  getTotalSkillCount,
19
22
  } from './prompts.js';
20
- import { installSkills, installSpecificSkills, listInstalledSkills, getAllCategoryIds } from './installer.js';
23
+ import { installSkills, installSpecificSkills, listInstalledSkills, getAllCategoryIds, updateInstalledSkills, uninstallAllSkills, uninstallSpecificSkills, getInstalledSkillPaths, getInstalledSkillsForSelection } from './installer.js';
21
24
 
22
25
  /**
23
26
  * Sleep utility
@@ -77,14 +80,20 @@ async function interactiveFlow() {
77
80
  }
78
81
 
79
82
  if (menuAction === 'update') {
80
- // Update all skills
83
+ // Update only installed skills
81
84
  showMenuHeader();
82
- console.log(chalk.cyan(' Updating all skills...'));
83
- console.log();
84
- const categories = getAllCategoryIds();
85
- await installSkills(categories, agents);
86
- console.log();
87
- console.log(chalk.green(' ✓ All skills updated!'));
85
+ const installedPaths = getInstalledSkillPaths();
86
+ if (installedPaths.length === 0) {
87
+ console.log(chalk.yellow(' No skills installed to update.'));
88
+ console.log();
89
+ console.log(chalk.dim(' Install some skills first.'));
90
+ } else {
91
+ console.log(chalk.cyan(` Updating ${installedPaths.length} installed skills...`));
92
+ console.log();
93
+ await updateInstalledSkills(agents);
94
+ console.log();
95
+ console.log(chalk.green(' ✓ All installed skills updated!'));
96
+ }
88
97
  const afterUpdate = await askAfterAction();
89
98
  if (afterUpdate === 'exit') {
90
99
  console.log(chalk.dim(' Goodbye!'));
@@ -94,6 +103,69 @@ async function interactiveFlow() {
94
103
  continue step2_menu;
95
104
  }
96
105
 
106
+ if (menuAction === 'uninstall') {
107
+ // Uninstall skills
108
+ step_uninstall:
109
+ while (true) {
110
+ showMenuHeader();
111
+ const installedSkills = getInstalledSkillsForSelection();
112
+
113
+ if (installedSkills.length === 0) {
114
+ console.log(chalk.yellow(' No skills installed to uninstall.'));
115
+ break;
116
+ }
117
+
118
+ const uninstallChoice = await askUninstallChoice();
119
+
120
+ if (uninstallChoice === 'back') {
121
+ break;
122
+ }
123
+
124
+ if (uninstallChoice === 'all') {
125
+ // Uninstall everything
126
+ const confirmAction = await askConfirmUninstall(installedSkills.length);
127
+ if (confirmAction === 'confirm') {
128
+ console.log();
129
+ await uninstallAllSkills(agents);
130
+ console.log();
131
+ console.log(chalk.green(' ✓ All skills uninstalled!'));
132
+ }
133
+ break;
134
+ }
135
+
136
+ if (uninstallChoice === 'select') {
137
+ // Select specific skills to uninstall
138
+ showMenuHeader();
139
+ const result = await askSelectSkillsToUninstall(installedSkills);
140
+
141
+ if (result.action === 'back') {
142
+ continue step_uninstall;
143
+ }
144
+ if (result.action === 'retry') {
145
+ continue step_uninstall;
146
+ }
147
+
148
+ // Confirm uninstall
149
+ const confirmAction = await askConfirmUninstall(result.skills.length);
150
+ if (confirmAction === 'confirm') {
151
+ console.log();
152
+ await uninstallSpecificSkills(result.skills, agents);
153
+ console.log();
154
+ console.log(chalk.green(` ✓ ${result.skills.length} skill${result.skills.length !== 1 ? 's' : ''} uninstalled!`));
155
+ }
156
+ break;
157
+ }
158
+ }
159
+
160
+ const afterUninstall = await askAfterAction();
161
+ if (afterUninstall === 'exit') {
162
+ console.log(chalk.dim(' Goodbye!'));
163
+ console.log();
164
+ return;
165
+ }
166
+ continue step2_menu;
167
+ }
168
+
97
169
  // STEP 3: Choose what to install (menuAction === 'install')
98
170
  step3_choice:
99
171
  while (true) {
@@ -220,14 +292,18 @@ async function commandMode(options) {
220
292
  }
221
293
 
222
294
  if (options.command === 'update') {
223
- console.log(chalk.cyan('Updating skills...'));
224
295
  const agents = detectAgents();
225
296
  if (agents.length === 0) {
226
297
  console.log(chalk.yellow('No agents detected.'));
227
298
  return;
228
299
  }
229
- const categories = getAllCategoryIds();
230
- await installSkills(categories, agents);
300
+ const installedPaths = getInstalledSkillPaths();
301
+ if (installedPaths.length === 0) {
302
+ console.log(chalk.yellow('No skills installed to update.'));
303
+ return;
304
+ }
305
+ console.log(chalk.cyan(`Updating ${installedPaths.length} installed skills...`));
306
+ await updateInstalledSkills(agents);
231
307
  console.log(chalk.green('✓ Skills updated!'));
232
308
  return;
233
309
  }
package/src/installer.js CHANGED
@@ -1,11 +1,11 @@
1
- import { existsSync, mkdirSync, symlinkSync, readdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
1
+ import { existsSync, mkdirSync, symlinkSync, readdirSync, readFileSync, writeFileSync, rmSync, lstatSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join, basename, dirname } from 'path';
4
4
  import { execSync } from 'child_process';
5
5
  import chalk from 'chalk';
6
6
  import ora from 'ora';
7
7
 
8
- const REPO_URL = 'https://github.com/zechenzhangAGI/AI-research-SKILLs';
8
+ const REPO_URL = 'https://github.com/Orchestra-Research/AI-research-SKILLs';
9
9
  const CANONICAL_DIR = join(homedir(), '.orchestra', 'skills');
10
10
  const LOCK_FILE = join(homedir(), '.orchestra', '.lock.json');
11
11
 
@@ -389,3 +389,223 @@ export function getAllCategoryIds() {
389
389
  '20-ml-paper-writing',
390
390
  ];
391
391
  }
392
+
393
+ /**
394
+ * Get installed skill paths for updating
395
+ * Returns array like ['06-post-training/verl', '20-ml-paper-writing']
396
+ */
397
+ export function getInstalledSkillPaths() {
398
+ if (!existsSync(CANONICAL_DIR)) {
399
+ return [];
400
+ }
401
+
402
+ const skillPaths = [];
403
+ const categories = readdirSync(CANONICAL_DIR, { withFileTypes: true })
404
+ .filter(d => d.isDirectory())
405
+ .map(d => d.name);
406
+
407
+ for (const category of categories) {
408
+ const categoryPath = join(CANONICAL_DIR, category);
409
+
410
+ // Check if it's a standalone skill (has SKILL.md directly)
411
+ const standaloneSkill = join(categoryPath, 'SKILL.md');
412
+ if (existsSync(standaloneSkill)) {
413
+ skillPaths.push(category);
414
+ } else {
415
+ // It's a category with nested skills
416
+ const skills = readdirSync(categoryPath, { withFileTypes: true })
417
+ .filter(d => d.isDirectory() && existsSync(join(categoryPath, d.name, 'SKILL.md')))
418
+ .map(d => d.name);
419
+
420
+ for (const skill of skills) {
421
+ skillPaths.push(`${category}/${skill}`);
422
+ }
423
+ }
424
+ }
425
+
426
+ return skillPaths;
427
+ }
428
+
429
+ /**
430
+ * Update only installed skills (re-download from GitHub)
431
+ */
432
+ export async function updateInstalledSkills(agents) {
433
+ const installedPaths = getInstalledSkillPaths();
434
+
435
+ if (installedPaths.length === 0) {
436
+ console.log(chalk.yellow(' No skills installed to update.'));
437
+ return 0;
438
+ }
439
+
440
+ const spinner = ora('Updating from GitHub...').start();
441
+
442
+ try {
443
+ // Download only the installed skills
444
+ const skills = await downloadSpecificSkills(installedPaths, spinner);
445
+ spinner.succeed(`Updated ${skills.length} skills`);
446
+
447
+ // Re-create symlinks for each agent
448
+ spinner.start('Refreshing symlinks...');
449
+
450
+ for (const agent of agents) {
451
+ const count = createSymlinks(agent, skills, spinner);
452
+ console.log(` ${chalk.green('✓')} ${agent.name.padEnd(14)} ${chalk.dim('→')} ${agent.skillsPath.replace(homedir(), '~').padEnd(25)} ${chalk.green(count + ' skills')}`);
453
+ }
454
+
455
+ spinner.stop();
456
+
457
+ // Update lock file
458
+ const lock = readLock();
459
+ lock.version = '1.0.0';
460
+ lock.installedAt = new Date().toISOString();
461
+ lock.skills = skills;
462
+ lock.agents = agents.map(a => a.id);
463
+ writeLock(lock);
464
+
465
+ return skills.length;
466
+ } catch (error) {
467
+ spinner.fail('Update failed');
468
+ throw error;
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Uninstall all skills
474
+ */
475
+ export async function uninstallAllSkills(agents) {
476
+ const spinner = ora('Removing skills...').start();
477
+
478
+ try {
479
+ // Remove symlinks from each agent
480
+ for (const agent of agents) {
481
+ if (existsSync(agent.skillsPath)) {
482
+ const entries = readdirSync(agent.skillsPath, { withFileTypes: true });
483
+ for (const entry of entries) {
484
+ const linkPath = join(agent.skillsPath, entry.name);
485
+ // Only remove if it's a symlink pointing to our canonical dir
486
+ try {
487
+ const stats = lstatSync(linkPath);
488
+ if (stats.isSymbolicLink()) {
489
+ rmSync(linkPath, { force: true });
490
+ }
491
+ } catch {
492
+ // Ignore errors
493
+ }
494
+ }
495
+ }
496
+ console.log(` ${chalk.green('✓')} Removed symlinks from ${agent.name}`);
497
+ }
498
+
499
+ // Remove canonical skills directory
500
+ if (existsSync(CANONICAL_DIR)) {
501
+ rmSync(CANONICAL_DIR, { recursive: true, force: true });
502
+ console.log(` ${chalk.green('✓')} Removed ${CANONICAL_DIR.replace(homedir(), '~')}`);
503
+ }
504
+
505
+ // Remove lock file
506
+ if (existsSync(LOCK_FILE)) {
507
+ rmSync(LOCK_FILE, { force: true });
508
+ }
509
+
510
+ spinner.stop();
511
+ return true;
512
+ } catch (error) {
513
+ spinner.fail('Uninstall failed');
514
+ throw error;
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Uninstall specific skills
520
+ * @param {Array<string>} skillPaths - Paths like ['06-post-training/verl', '20-ml-paper-writing']
521
+ * @param {Array} agents - List of agents to remove symlinks from
522
+ */
523
+ export async function uninstallSpecificSkills(skillPaths, agents) {
524
+ const spinner = ora('Removing selected skills...').start();
525
+
526
+ try {
527
+ for (const skillPath of skillPaths) {
528
+ const parts = skillPath.split('/');
529
+ const categoryId = parts[0];
530
+ const skillName = parts[1] || null;
531
+
532
+ // Determine the link name (what appears in agent's skills folder)
533
+ const linkName = skillName || categoryId;
534
+
535
+ // Remove symlinks from each agent
536
+ for (const agent of agents) {
537
+ const linkPath = join(agent.skillsPath, linkName);
538
+ try {
539
+ if (existsSync(linkPath)) {
540
+ const stats = lstatSync(linkPath);
541
+ if (stats.isSymbolicLink()) {
542
+ rmSync(linkPath, { force: true });
543
+ }
544
+ }
545
+ } catch {
546
+ // Ignore errors
547
+ }
548
+ }
549
+
550
+ // Remove from canonical directory
551
+ if (skillName) {
552
+ // Nested skill like '06-post-training/verl'
553
+ const skillDir = join(CANONICAL_DIR, categoryId, skillName);
554
+ if (existsSync(skillDir)) {
555
+ rmSync(skillDir, { recursive: true, force: true });
556
+ }
557
+ // Clean up empty category folder
558
+ const categoryDir = join(CANONICAL_DIR, categoryId);
559
+ if (existsSync(categoryDir)) {
560
+ const remaining = readdirSync(categoryDir);
561
+ if (remaining.length === 0) {
562
+ rmSync(categoryDir, { recursive: true, force: true });
563
+ }
564
+ }
565
+ } else {
566
+ // Standalone skill like '20-ml-paper-writing'
567
+ const skillDir = join(CANONICAL_DIR, categoryId);
568
+ if (existsSync(skillDir)) {
569
+ rmSync(skillDir, { recursive: true, force: true });
570
+ }
571
+ }
572
+
573
+ spinner.text = `Removed ${linkName}`;
574
+ }
575
+
576
+ spinner.succeed(`Removed ${skillPaths.length} skill${skillPaths.length !== 1 ? 's' : ''}`);
577
+
578
+ // Update lock file
579
+ const lock = readLock();
580
+ if (lock.skills) {
581
+ lock.skills = lock.skills.filter(s => {
582
+ const path = s.standalone ? s.category : `${s.category}/${s.skill}`;
583
+ return !skillPaths.includes(path);
584
+ });
585
+ writeLock(lock);
586
+ }
587
+
588
+ return skillPaths.length;
589
+ } catch (error) {
590
+ spinner.fail('Uninstall failed');
591
+ throw error;
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Get installed skills with display info for selection
597
+ * Returns array of { path, name, category } for UI
598
+ */
599
+ export function getInstalledSkillsForSelection() {
600
+ const paths = getInstalledSkillPaths();
601
+ return paths.map(path => {
602
+ const parts = path.split('/');
603
+ if (parts.length === 1) {
604
+ // Standalone skill
605
+ return { path, name: parts[0], category: 'Standalone', standalone: true };
606
+ } else {
607
+ // Nested skill
608
+ return { path, name: parts[1], category: parts[0], standalone: false };
609
+ }
610
+ });
611
+ }
package/src/prompts.js CHANGED
@@ -104,7 +104,8 @@ export async function askMainMenuAction() {
104
104
  choices: [
105
105
  { name: 'Install new skills', value: 'install' },
106
106
  { name: 'View installed skills', value: 'view' },
107
- { name: 'Update all skills', value: 'update' },
107
+ { name: 'Update installed skills', value: 'update' },
108
+ { name: 'Uninstall all skills', value: 'uninstall' },
108
109
  new inquirer.Separator(' '),
109
110
  { name: chalk.dim('Exit'), value: 'exit' },
110
111
  ],
@@ -114,6 +115,98 @@ export async function askMainMenuAction() {
114
115
  return action;
115
116
  }
116
117
 
118
+ /**
119
+ * Ask what to uninstall
120
+ */
121
+ export async function askUninstallChoice() {
122
+ console.log();
123
+ console.log(chalk.dim(' What would you like to uninstall?'));
124
+ console.log();
125
+
126
+ const { choice } = await inquirer.prompt([
127
+ {
128
+ type: 'list',
129
+ name: 'choice',
130
+ message: ' ',
131
+ choices: [
132
+ { name: 'Select specific skills', value: 'select' },
133
+ { name: chalk.red('Uninstall everything'), value: 'all' },
134
+ new inquirer.Separator(' '),
135
+ { name: chalk.dim('← Back'), value: 'back' },
136
+ ],
137
+ prefix: ' ',
138
+ },
139
+ ]);
140
+ return choice;
141
+ }
142
+
143
+ /**
144
+ * Ask which installed skills to uninstall
145
+ */
146
+ export async function askSelectSkillsToUninstall(installedSkills) {
147
+ console.log();
148
+ console.log(chalk.dim(' Select skills to uninstall:'));
149
+ console.log(chalk.dim(' (Space to select, Enter to confirm)'));
150
+ console.log();
151
+
152
+ const { skills } = await inquirer.prompt([
153
+ {
154
+ type: 'checkbox',
155
+ name: 'skills',
156
+ message: ' ',
157
+ choices: installedSkills.map(skill => ({
158
+ name: `${skill.name.padEnd(25)} ${chalk.dim(skill.category)}`,
159
+ value: skill.path,
160
+ short: skill.name,
161
+ })),
162
+ prefix: ' ',
163
+ pageSize: 15,
164
+ },
165
+ ]);
166
+
167
+ if (skills.length === 0) {
168
+ console.log();
169
+ const { action } = await inquirer.prompt([
170
+ {
171
+ type: 'list',
172
+ name: 'action',
173
+ message: chalk.yellow('No skills selected'),
174
+ choices: [
175
+ { name: 'Try again', value: 'retry' },
176
+ { name: chalk.dim('← Back'), value: 'back' },
177
+ ],
178
+ prefix: ' ',
179
+ },
180
+ ]);
181
+ return { skills: [], action };
182
+ }
183
+
184
+ return { skills, action: 'confirm' };
185
+ }
186
+
187
+ /**
188
+ * Ask to confirm uninstall
189
+ */
190
+ export async function askConfirmUninstall(count) {
191
+ console.log();
192
+ console.log(chalk.yellow(` This will remove ${count} skill${count !== 1 ? 's' : ''} and their symlinks.`));
193
+ console.log();
194
+
195
+ const { action } = await inquirer.prompt([
196
+ {
197
+ type: 'list',
198
+ name: 'action',
199
+ message: ' ',
200
+ choices: [
201
+ { name: chalk.red('Yes, uninstall'), value: 'confirm' },
202
+ { name: chalk.dim('← Back'), value: 'back' },
203
+ ],
204
+ prefix: ' ',
205
+ },
206
+ ]);
207
+ return action;
208
+ }
209
+
117
210
  /**
118
211
  * Ask what to install
119
212
  */