@supercorks/skills-installer 1.11.1 → 1.12.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @supercorks/skills-installer
2
2
 
3
- Interactive CLI installer for AI agent skills and subagents. Selectively install resources for GitHub Copilot, Codex, Claude, and other AI assistants using Git sparse-checkout.
3
+ Interactive CLI installer for AI agent skills and subagents. Selectively install resources for GitHub Copilot, Codex, Claude, and other AI assistants using Git sparse-checkout and Codex agent conversion where needed.
4
4
 
5
5
  ## Usage
6
6
 
@@ -18,13 +18,23 @@ npx @supercorks/skills-installer install
18
18
 
19
19
  1. **Choose installation type** - Install skills, subagents, or both.
20
20
 
21
- 2. **Choose installation path(s)** - Select one or more locations where resources should be installed:
22
- - `.github/skills/` (Copilot)
23
- - `~/.codex/skills/` (Codex)
24
- - `.claude/skills/` (Claude)
25
- - `.github/agents/` (Copilot)
26
- - `.agents/agents/` (Codex)
27
- - `.claude/agents/` (Claude)
21
+ 2. **Choose installation path(s)** - Select one or more locations where resources should be installed. The installer labels each option with harness, scope, and available count, for example `.github/skills/ (copilot | local | 24 skills)`.
22
+
23
+ Skills:
24
+ - `.github/skills/` (copilot | local)
25
+ - `~/.copilot/skills/` (copilot | global)
26
+ - `.agents/skills/` (codex | local)
27
+ - `~/.agents/skills/` (codex | global)
28
+ - `.claude/skills/` (claude | local)
29
+ - `~/.claude/skills/` (claude | global)
30
+
31
+ Agents:
32
+ - `.github/agents/` (copilot | local)
33
+ - `~/.copilot/agents/` (copilot | global)
34
+ - `.claude/agents/` (claude | local)
35
+ - `~/.claude/agents/` (claude | global)
36
+ - `.codex/agents/` (codex | local, installed as converted TOML agents)
37
+ - `~/.codex/agents/` (codex | global, installed as converted TOML agents)
28
38
  - Custom path of your choice
29
39
 
30
40
  3. **Gitignore option** - If launched from inside a git repository, optionally add the installation path to `.gitignore`
@@ -36,7 +46,9 @@ npx @supercorks/skills-installer install
36
46
  - Use `A` to toggle all
37
47
  - Press `ENTER` to confirm
38
48
 
39
- 5. **Sparse clone** - Only downloads selected skills/subagents using Git sparse-checkout, keeping the download minimal while preserving full git functionality.
49
+ 5. **Install backend**
50
+ - Skills and Markdown-based agents use Git sparse-checkout for minimal download while preserving full git functionality.
51
+ - Codex agents are generated as TOML files from the source Markdown agent definitions.
40
52
 
41
53
  ## Installed repositories
42
54
 
@@ -48,6 +60,8 @@ npx @supercorks/skills-installer install
48
60
  - **Minimal download** - Uses `git clone --filter=blob:none` for efficient cloning
49
61
  - **Push capable** - The sparse clone preserves the full git history, allowing you to commit and push changes
50
62
  - **Auto-discovery** - Fetches the latest skill list from the repository
63
+ - **Global and local targets** - Offers documented project/user locations for Copilot, Codex, and Claude where the resource format is compatible
64
+ - **Codex agent conversion** - Converts Markdown subagents into Codex TOML custom agents for `.codex/agents/` targets
51
65
  - **Recursive directory creation** - Custom paths are created automatically
52
66
 
53
67
  ## Requirements
package/bin/install.js CHANGED
@@ -37,13 +37,15 @@ import {
37
37
  checkSubagentsForUpdates
38
38
  } from '../lib/git.js';
39
39
  import { createRequire } from 'module';
40
+ import { allAgentDetectionTargets, allSkillDetectionTargets, getAgentInstallMode } from '../lib/install-targets.js';
41
+ import { checkCodexAgentUpdates, listInstalledCodexAgents, syncCodexAgents } from '../lib/codex-agents.js';
40
42
 
41
43
  const require = createRequire(import.meta.url);
42
44
  const { version: VERSION } = require('../package.json');
43
45
 
44
46
  // Common installation paths to check for existing installations
45
- const SKILL_PATHS = ['.github/skills/', '~/.codex/skills/', '.claude/skills/'];
46
- const AGENT_PATHS = ['.github/agents/', '.agents/agents/', '.claude/agents/'];
47
+ const SKILL_PATHS = allSkillDetectionTargets().map(target => target.path);
48
+ const AGENT_PATHS = allAgentDetectionTargets().map(target => target.path);
47
49
 
48
50
  function resolveInstallPath(path) {
49
51
  if (path === '~') return homedir();
@@ -92,6 +94,24 @@ async function detectExistingAgentInstallations() {
92
94
 
93
95
  for (const path of AGENT_PATHS) {
94
96
  const absolutePath = resolveInstallPath(path);
97
+ const installMode = getAgentInstallMode(path);
98
+
99
+ if (installMode === 'codex-toml') {
100
+ try {
101
+ const agents = await listInstalledCodexAgents(absolutePath);
102
+ if (agents.length > 0) {
103
+ installations.push({
104
+ path,
105
+ agentCount: agents.length,
106
+ agents
107
+ });
108
+ }
109
+ } catch {
110
+ // Ignore errors reading existing installations
111
+ }
112
+ continue;
113
+ }
114
+
95
115
  const gitDir = join(absolutePath, '.git');
96
116
 
97
117
  if (existsSync(gitDir)) {
@@ -220,7 +240,7 @@ async function runSkillsInstall() {
220
240
  const existingInstalls = await detectExistingSkillInstallations();
221
241
 
222
242
  // Ask where to install (showing existing installations if any)
223
- const installTargets = await promptInstallPath(existingInstalls);
243
+ const installTargets = await promptInstallPath(existingInstalls, skills.length);
224
244
 
225
245
  for (let i = 0; i < installTargets.length; i++) {
226
246
  const target = installTargets[i];
@@ -380,7 +400,7 @@ async function runSubagentsInstall() {
380
400
  const existingInstalls = await detectExistingAgentInstallations();
381
401
 
382
402
  // Ask where to install (showing existing installations if any)
383
- const installTargets = await promptAgentInstallPath(existingInstalls);
403
+ const installTargets = await promptAgentInstallPath(existingInstalls, subagents.length);
384
404
 
385
405
  for (let i = 0; i < installTargets.length; i++) {
386
406
  const target = installTargets[i];
@@ -400,15 +420,16 @@ async function runSubagentsInstall() {
400
420
  async function runSubagentsInstallForTarget(subagents, existingInstalls, target) {
401
421
  const { path: installPath, isExisting } = target;
402
422
  const absoluteInstallPath = resolveInstallPath(installPath);
423
+ const installMode = getAgentInstallMode(installPath);
403
424
  const gitDir = join(absoluteInstallPath, '.git');
404
- const hasExistingRepo = existsSync(gitDir);
425
+ const hasExistingRepo = installMode === 'sparse-git' && existsSync(gitDir);
405
426
 
406
427
  // Get currently installed subagents if managing existing installation
407
428
  let installedAgents = [];
408
429
  if (isExisting) {
409
430
  const existingInstall = existingInstalls.find(i => i.path === installPath);
410
431
  installedAgents = existingInstall?.agents || [];
411
- } else {
432
+ } else if (installMode === 'sparse-git') {
412
433
  // Check if manually entered path has an existing installation
413
434
  if (hasExistingRepo) {
414
435
  try {
@@ -418,16 +439,27 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
418
439
  // Prompt will default to selecting all subagents.
419
440
  }
420
441
  }
442
+ } else {
443
+ try {
444
+ installedAgents = await listInstalledCodexAgents(absoluteInstallPath);
445
+ } catch {
446
+ // Ignore detection failures for custom Codex agent paths.
447
+ }
421
448
  }
422
449
 
423
- const isManageMode = isExisting || hasExistingRepo || installedAgents.length > 0;
450
+ const isManageMode = installMode === 'sparse-git'
451
+ ? (isExisting || hasExistingRepo || installedAgents.length > 0)
452
+ : installedAgents.length > 0;
424
453
 
425
454
  // Check for updates if in manage mode
426
455
  let subagentsNeedingUpdate = new Set();
427
456
  if (isManageMode) {
428
457
  const updateSpinner = showSpinner('Checking for available updates...');
429
458
  try {
430
- subagentsNeedingUpdate = await checkSubagentsForUpdates(absoluteInstallPath, installedAgents);
459
+ subagentsNeedingUpdate = installMode === 'sparse-git'
460
+ ? await checkSubagentsForUpdates(absoluteInstallPath, installedAgents)
461
+ : await checkCodexAgentUpdates(absoluteInstallPath, installedAgents);
462
+
431
463
  if (subagentsNeedingUpdate.size > 0) {
432
464
  updateSpinner.stop(`✅ Found ${subagentsNeedingUpdate.size} subagent${subagentsNeedingUpdate.size !== 1 ? 's' : ''} with updates available`);
433
465
  } else {
@@ -475,9 +507,15 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
475
507
  const updateSpinner = showSpinner('Updating subagents installation...');
476
508
 
477
509
  try {
478
- await updateSubagentsSparseCheckout(absoluteInstallPath, selectedAgents, (message) => {
479
- updateSpinner.stop(` ${message}`);
480
- });
510
+ if (installMode === 'sparse-git') {
511
+ await updateSubagentsSparseCheckout(absoluteInstallPath, selectedAgents, (message) => {
512
+ updateSpinner.stop(` ${message}`);
513
+ });
514
+ } else {
515
+ await syncCodexAgents(absoluteInstallPath, selectedAgents, (message) => {
516
+ updateSpinner.stop(` ${message}`);
517
+ });
518
+ }
481
519
  } catch (error) {
482
520
  updateSpinner.stop('❌ Update failed');
483
521
  showError(error.message);
@@ -490,9 +528,15 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
490
528
  const installSpinner = showSpinner('Installing selected subagents...');
491
529
 
492
530
  try {
493
- await sparseCloneSubagents(installPath, selectedAgents, (message) => {
494
- installSpinner.stop(` ${message}`);
495
- });
531
+ if (installMode === 'sparse-git') {
532
+ await sparseCloneSubagents(installPath, selectedAgents, (message) => {
533
+ installSpinner.stop(` ${message}`);
534
+ });
535
+ } else {
536
+ await syncCodexAgents(absoluteInstallPath, selectedAgents, (message) => {
537
+ installSpinner.stop(` ${message}`);
538
+ });
539
+ }
496
540
  } catch (error) {
497
541
  installSpinner.stop('❌ Installation failed');
498
542
  showError(error.message);
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Convert Markdown subagent definitions into Codex TOML custom agents.
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
6
+ import { join, resolve } from 'path';
7
+ import { homedir } from 'os';
8
+ import { fetchSubagentContent, humanizeAgentName, parseSubagentDefinition } from './subagents.js';
9
+
10
+ const GENERATED_COMMENT_PREFIX = '# Generated by @supercorks/skills-installer from ';
11
+
12
+ function resolvePath(path) {
13
+ if (path === '~') return homedir();
14
+ if (path.startsWith('~/')) return resolve(homedir(), path.slice(2));
15
+ return resolve(path);
16
+ }
17
+
18
+ export function codexTomlFilenameForAgent(agentFilename) {
19
+ return `${agentFilename.replace(/\.agent\.md$/i, '')}.toml`;
20
+ }
21
+
22
+ export function codexAgentNameForAgent(agentFilename, displayName = '') {
23
+ const preferred = displayName || agentFilename.replace(/\.agent\.md$/i, '');
24
+ return preferred
25
+ .trim()
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, '_')
28
+ .replace(/^_+|_+$/g, '') || 'agent';
29
+ }
30
+
31
+ function escapeTomlBasicString(value) {
32
+ return String(value)
33
+ .replace(/\\/g, '\\\\')
34
+ .replace(/"/g, '\\"')
35
+ .replace(/\n/g, ' ')
36
+ .trim();
37
+ }
38
+
39
+ function formatDeveloperInstructions(body) {
40
+ const normalizedBody = (body || '').replace(/\r\n/g, '\n').trim();
41
+ if (!normalizedBody) {
42
+ return "developer_instructions = '''\nAct as the requested specialist and complete the assigned task.\n'''";
43
+ }
44
+
45
+ if (!normalizedBody.includes("'''")) {
46
+ return `developer_instructions = '''\n${normalizedBody}\n'''`;
47
+ }
48
+
49
+ const escaped = normalizedBody
50
+ .replace(/\\/g, '\\\\')
51
+ .replace(/"""/g, '\\"\\"\\"');
52
+
53
+ return `developer_instructions = """\n${escaped}\n"""`;
54
+ }
55
+
56
+ export function convertSubagentMarkdownToCodexToml(content, agentFilename) {
57
+ const definition = parseSubagentDefinition(content, agentFilename);
58
+ const outputFilename = codexTomlFilenameForAgent(agentFilename);
59
+ const codexName = codexAgentNameForAgent(agentFilename, definition.name);
60
+ const description = definition.description || `${humanizeAgentName(agentFilename)} custom agent`;
61
+
62
+ const toml = [
63
+ `${GENERATED_COMMENT_PREFIX}${agentFilename}`,
64
+ `name = "${escapeTomlBasicString(codexName)}"`,
65
+ `description = "${escapeTomlBasicString(description)}"`,
66
+ formatDeveloperInstructions(definition.body),
67
+ '',
68
+ ].join('\n');
69
+
70
+ return {
71
+ sourceFilename: agentFilename,
72
+ outputFilename,
73
+ name: definition.name,
74
+ description,
75
+ toml,
76
+ };
77
+ }
78
+
79
+ function parseGeneratedSourceFilename(tomlContent, outputFilename) {
80
+ const commentMatch = tomlContent.match(/^# Generated by @supercorks\/skills-installer from ([^\n]+)$/m);
81
+ if (commentMatch) {
82
+ return commentMatch[1].trim();
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ function listGeneratedCodexAgentEntries(targetPath) {
89
+ const absolutePath = resolvePath(targetPath);
90
+ if (!existsSync(absolutePath)) {
91
+ return [];
92
+ }
93
+
94
+ return readdirSync(absolutePath)
95
+ .filter(name => name.endsWith('.toml'))
96
+ .map(outputFilename => {
97
+ const absoluteFilePath = join(absolutePath, outputFilename);
98
+ const content = readFileSync(absoluteFilePath, 'utf8');
99
+ const sourceFilename = parseGeneratedSourceFilename(content, outputFilename);
100
+ if (!sourceFilename) {
101
+ return null;
102
+ }
103
+
104
+ return {
105
+ sourceFilename,
106
+ outputFilename,
107
+ absoluteFilePath,
108
+ content,
109
+ };
110
+ })
111
+ .filter(Boolean);
112
+ }
113
+
114
+ export async function listInstalledCodexAgents(targetPath) {
115
+ return listGeneratedCodexAgentEntries(targetPath).map(entry => entry.sourceFilename);
116
+ }
117
+
118
+ export async function checkCodexAgentUpdates(targetPath, agentFilenames) {
119
+ const installedEntries = listGeneratedCodexAgentEntries(targetPath);
120
+ const bySourceFilename = new Map(installedEntries.map(entry => [entry.sourceFilename, entry]));
121
+ const needsUpdate = new Set();
122
+
123
+ for (const agentFilename of agentFilenames) {
124
+ const entry = bySourceFilename.get(agentFilename);
125
+ if (!entry) {
126
+ continue;
127
+ }
128
+
129
+ try {
130
+ const content = await fetchSubagentContent(agentFilename);
131
+ const converted = convertSubagentMarkdownToCodexToml(content, agentFilename);
132
+ if (converted.toml !== entry.content) {
133
+ needsUpdate.add(agentFilename);
134
+ }
135
+ } catch {
136
+ // Skip update markers when the remote file cannot be fetched.
137
+ }
138
+ }
139
+
140
+ return needsUpdate;
141
+ }
142
+
143
+ export async function syncCodexAgents(targetPath, agentFilenames, onProgress = () => {}) {
144
+ const absolutePath = resolvePath(targetPath);
145
+ mkdirSync(absolutePath, { recursive: true });
146
+
147
+ const existingEntries = listGeneratedCodexAgentEntries(absolutePath);
148
+ const selectedSet = new Set(agentFilenames);
149
+
150
+ for (const entry of existingEntries) {
151
+ if (!selectedSet.has(entry.sourceFilename) && existsSync(entry.absoluteFilePath)) {
152
+ rmSync(entry.absoluteFilePath, { force: true });
153
+ }
154
+ }
155
+
156
+ for (let index = 0; index < agentFilenames.length; index += 1) {
157
+ const agentFilename = agentFilenames[index];
158
+ onProgress(`Converting ${index + 1}/${agentFilenames.length}: ${agentFilename}`);
159
+ const content = await fetchSubagentContent(agentFilename);
160
+ const converted = convertSubagentMarkdownToCodexToml(content, agentFilename);
161
+ writeFileSync(join(absolutePath, converted.outputFilename), converted.toml, 'utf8');
162
+ }
163
+
164
+ onProgress('Done!');
165
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Supported install targets for skills and agents.
3
+ */
4
+
5
+ export const SKILL_INSTALL_TARGETS = [
6
+ { path: '.github/skills/', harness: 'copilot', scope: 'local' },
7
+ { path: '~/.copilot/skills/', harness: 'copilot', scope: 'global' },
8
+ { path: '.agents/skills/', harness: 'codex', scope: 'local' },
9
+ { path: '~/.agents/skills/', harness: 'codex', scope: 'global' },
10
+ { path: '.claude/skills/', harness: 'claude', scope: 'local' },
11
+ { path: '~/.claude/skills/', harness: 'claude', scope: 'global' }
12
+ ];
13
+
14
+ export const LEGACY_SKILL_INSTALL_TARGETS = [
15
+ {
16
+ path: '~/.codex/skills/',
17
+ harness: 'codex',
18
+ scope: 'legacy global'
19
+ }
20
+ ];
21
+
22
+ export const AGENT_INSTALL_TARGETS = [
23
+ { path: '.github/agents/', harness: 'copilot', scope: 'local', installMode: 'sparse-git' },
24
+ { path: '~/.copilot/agents/', harness: 'copilot', scope: 'global', installMode: 'sparse-git' },
25
+ { path: '.claude/agents/', harness: 'claude', scope: 'local', installMode: 'sparse-git' },
26
+ { path: '~/.claude/agents/', harness: 'claude', scope: 'global', installMode: 'sparse-git' },
27
+ { path: '.codex/agents/', harness: 'codex', scope: 'local', installMode: 'codex-toml' },
28
+ { path: '~/.codex/agents/', harness: 'codex', scope: 'global', installMode: 'codex-toml' }
29
+ ];
30
+
31
+ export const LEGACY_AGENT_INSTALL_TARGETS = [
32
+ {
33
+ path: '.agents/agents/',
34
+ harness: 'codex',
35
+ scope: 'legacy local'
36
+ }
37
+ ];
38
+
39
+ export function allSkillDetectionTargets() {
40
+ return [...SKILL_INSTALL_TARGETS, ...LEGACY_SKILL_INSTALL_TARGETS];
41
+ }
42
+
43
+ export function allAgentDetectionTargets() {
44
+ return [...AGENT_INSTALL_TARGETS, ...LEGACY_AGENT_INSTALL_TARGETS];
45
+ }
46
+
47
+ export function getAgentInstallMode(path) {
48
+ const exactTarget = getTargetByPath(AGENT_INSTALL_TARGETS, path);
49
+ if (exactTarget?.installMode) {
50
+ return exactTarget.installMode;
51
+ }
52
+
53
+ const normalizedPath = path.replace(/\\/g, '/').replace(/\/+$/, '');
54
+ if (normalizedPath.endsWith('/.codex/agents') || normalizedPath === '.codex/agents' || normalizedPath === '~/.codex/agents') {
55
+ return 'codex-toml';
56
+ }
57
+
58
+ return 'sparse-git';
59
+ }
60
+
61
+ export function getTargetByPath(targets, path) {
62
+ return targets.find(target => target.path === path);
63
+ }
64
+
65
+ export function formatTargetLabel(target, count, noun, { installed = false } = {}) {
66
+ const plural = count === 1 ? noun : `${noun}s`;
67
+ const countText = installed ? `${count} ${plural} installed` : `${count} ${plural}`;
68
+ const detail = target
69
+ ? `${target.harness} | ${target.scope} | ${countText}`
70
+ : countText;
71
+
72
+ return `${target?.path || ''} (${detail})`;
73
+ }
package/lib/prompts.js CHANGED
@@ -4,18 +4,20 @@
4
4
 
5
5
  import inquirer from 'inquirer';
6
6
  import * as readline from 'readline';
7
+ import {
8
+ AGENT_INSTALL_TARGETS,
9
+ SKILL_INSTALL_TARGETS,
10
+ allAgentDetectionTargets,
11
+ allSkillDetectionTargets,
12
+ formatTargetLabel,
13
+ getTargetByPath
14
+ } from './install-targets.js';
7
15
 
8
16
  const SKILL_PATH_CHOICES = {
9
- GITHUB: '.github/skills/',
10
- CODEX_HOME: '~/.codex/skills/',
11
- CLAUDE: '.claude/skills/',
12
17
  CUSTOM: '__custom__'
13
18
  };
14
19
 
15
20
  const AGENT_PATH_CHOICES = {
16
- GITHUB: '.github/agents/',
17
- CODEX: '.agents/agents/',
18
- CLAUDE: '.claude/agents/',
19
21
  CUSTOM: '__custom__'
20
22
  };
21
23
 
@@ -61,17 +63,22 @@ export async function promptInstallType() {
61
63
  /**
62
64
  * Prompt user to select one or more installation paths, showing existing installations
63
65
  * @param {Array<{path: string, skillCount: number}>} existingInstalls - Detected existing installations
66
+ * @param {number} availableSkillCount - Number of skills available for new installations
64
67
  * @returns {Promise<Array<{path: string, isExisting: boolean}>>} Selected paths and whether each is existing
65
68
  */
66
- export async function promptInstallPath(existingInstalls = []) {
69
+ export async function promptInstallPath(existingInstalls = [], availableSkillCount = 0) {
67
70
  const choices = [];
68
71
  const existingPaths = existingInstalls.map(i => i.path);
72
+ const detectionTargets = allSkillDetectionTargets();
69
73
 
70
74
  // Add existing installations at the top
71
75
  if (existingInstalls.length > 0) {
72
76
  existingInstalls.forEach(install => {
77
+ const target = getTargetByPath(detectionTargets, install.path);
73
78
  choices.push({
74
- name: `${install.path} (${install.skillCount} skill${install.skillCount !== 1 ? 's' : ''} installed)`,
79
+ name: target
80
+ ? formatTargetLabel(target, install.skillCount, 'skill', { installed: true })
81
+ : `${install.path} (${install.skillCount} skill${install.skillCount !== 1 ? 's' : ''} installed)`,
75
82
  value: install.path
76
83
  });
77
84
  });
@@ -79,15 +86,12 @@ export async function promptInstallPath(existingInstalls = []) {
79
86
  }
80
87
 
81
88
  // Standard path options
82
- const standardPaths = [
83
- { path: SKILL_PATH_CHOICES.GITHUB, label: `${SKILL_PATH_CHOICES.GITHUB} (Copilot)` },
84
- { path: SKILL_PATH_CHOICES.CODEX_HOME, label: `${SKILL_PATH_CHOICES.CODEX_HOME} (Codex)` },
85
- { path: SKILL_PATH_CHOICES.CLAUDE, label: `${SKILL_PATH_CHOICES.CLAUDE} (Claude)` }
86
- ];
87
-
88
- standardPaths.forEach(({ path, label }) => {
89
- if (!existingPaths.includes(path)) {
90
- choices.push({ name: label, value: path });
89
+ SKILL_INSTALL_TARGETS.forEach(target => {
90
+ if (!existingPaths.includes(target.path)) {
91
+ choices.push({
92
+ name: formatTargetLabel(target, availableSkillCount, 'skill'),
93
+ value: target.path
94
+ });
91
95
  }
92
96
  });
93
97
 
@@ -144,17 +148,22 @@ export async function promptInstallPath(existingInstalls = []) {
144
148
  /**
145
149
  * Prompt user to select one or more subagent installation paths
146
150
  * @param {Array<{path: string, agentCount: number}>} existingInstalls - Detected existing installations
151
+ * @param {number} availableAgentCount - Number of agents available for new installations
147
152
  * @returns {Promise<Array<{path: string, isExisting: boolean}>>} Selected paths and whether each is existing
148
153
  */
149
- export async function promptAgentInstallPath(existingInstalls = []) {
154
+ export async function promptAgentInstallPath(existingInstalls = [], availableAgentCount = 0) {
150
155
  const choices = [];
151
156
  const existingPaths = existingInstalls.map(i => i.path);
157
+ const detectionTargets = allAgentDetectionTargets();
152
158
 
153
159
  // Add existing installations at the top
154
160
  if (existingInstalls.length > 0) {
155
161
  existingInstalls.forEach(install => {
162
+ const target = getTargetByPath(detectionTargets, install.path);
156
163
  choices.push({
157
- name: `${install.path} (${install.agentCount} agent${install.agentCount !== 1 ? 's' : ''} installed)`,
164
+ name: target
165
+ ? formatTargetLabel(target, install.agentCount, 'agent', { installed: true })
166
+ : `${install.path} (${install.agentCount} agent${install.agentCount !== 1 ? 's' : ''} installed)`,
158
167
  value: install.path
159
168
  });
160
169
  });
@@ -162,15 +171,13 @@ export async function promptAgentInstallPath(existingInstalls = []) {
162
171
  }
163
172
 
164
173
  // Standard path options
165
- const standardPaths = [
166
- { path: AGENT_PATH_CHOICES.GITHUB, label: `${AGENT_PATH_CHOICES.GITHUB} (Copilot)` },
167
- { path: AGENT_PATH_CHOICES.CODEX, label: `${AGENT_PATH_CHOICES.CODEX} (Codex)` },
168
- { path: AGENT_PATH_CHOICES.CLAUDE, label: `${AGENT_PATH_CHOICES.CLAUDE} (Claude)` }
169
- ];
170
-
171
- standardPaths.forEach(({ path, label }) => {
172
- if (!existingPaths.includes(path)) {
173
- choices.push({ name: label, value: path });
174
+ AGENT_INSTALL_TARGETS.forEach(target => {
175
+ if (!existingPaths.includes(target.path)) {
176
+ choices.push({
177
+ name: formatTargetLabel(target, availableAgentCount, 'agent'),
178
+ value: target.path,
179
+ disabled: target.disabledReason
180
+ });
174
181
  }
175
182
  });
176
183
 
package/lib/subagents.js CHANGED
@@ -17,6 +17,29 @@ function humanizeAgentName(filename) {
17
17
  .join(' ');
18
18
  }
19
19
 
20
+ function extractSubagentSections(content) {
21
+ const standardMatch = content.match(/^(---\s*\n[\s\S]*?\n---)(?:\s*\n)?/);
22
+ if (standardMatch) {
23
+ return {
24
+ frontmatter: standardMatch[1].replace(/^---\s*\n|\n---$/g, ''),
25
+ body: content.slice(standardMatch[0].length).trim(),
26
+ };
27
+ }
28
+
29
+ const chatAgentMatch = content.match(/^```chatagent\s*\n---\s*\n([\s\S]*?)\n---\s*\n```(?:\s*\n)?/);
30
+ if (chatAgentMatch) {
31
+ return {
32
+ frontmatter: chatAgentMatch[1],
33
+ body: content.slice(chatAgentMatch[0].length).trim(),
34
+ };
35
+ }
36
+
37
+ return {
38
+ frontmatter: '',
39
+ body: content.trim(),
40
+ };
41
+ }
42
+
20
43
  /**
21
44
  * Fetch the list of subagent files from the repository
22
45
  * Subagents are .agent.md files at the repo root
@@ -54,6 +77,20 @@ export async function fetchAvailableSubagents() {
54
77
  * @returns {Promise<{name: string, description: string}>}
55
78
  */
56
79
  export async function fetchSubagentMetadata(filename) {
80
+ try {
81
+ const content = await fetchSubagentContent(filename);
82
+ return parseSubagentFrontmatter(content);
83
+ } catch (error) {
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Fetch the raw source for a subagent file
90
+ * @param {string} filename - The agent filename
91
+ * @returns {Promise<string>}
92
+ */
93
+ export async function fetchSubagentContent(filename) {
57
94
  const fileUrl = `${GITHUB_API}/repos/${SUBAGENTS_REPO_OWNER}/${SUBAGENTS_REPO_NAME}/contents/${filename}`;
58
95
 
59
96
  try {
@@ -66,9 +103,7 @@ export async function fetchSubagentMetadata(filename) {
66
103
  }
67
104
 
68
105
  const data = await response.json();
69
- const content = Buffer.from(data.content, 'base64').toString('utf-8');
70
-
71
- return parseSubagentFrontmatter(content);
106
+ return Buffer.from(data.content, 'base64').toString('utf-8');
72
107
  } catch (error) {
73
108
  throw error;
74
109
  }
@@ -81,13 +116,7 @@ export async function fetchSubagentMetadata(filename) {
81
116
  * @returns {{name: string, description: string}}
82
117
  */
83
118
  function parseSubagentFrontmatter(content) {
84
- // Try to match standard --- frontmatter
85
- const standardMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
86
-
87
- // Try to match ```chatagent fenced frontmatter
88
- const chatAgentMatch = content.match(/```chatagent\s*\n---\s*\n([\s\S]*?)\n---/);
89
-
90
- const frontmatterContent = standardMatch?.[1] || chatAgentMatch?.[1];
119
+ const { frontmatter: frontmatterContent } = extractSubagentSections(content);
91
120
 
92
121
  if (!frontmatterContent) {
93
122
  return { name: '', description: '' };
@@ -102,6 +131,24 @@ function parseSubagentFrontmatter(content) {
102
131
  };
103
132
  }
104
133
 
134
+ /**
135
+ * Parse a subagent file into metadata and body content.
136
+ * @param {string} content - The .agent.md file content
137
+ * @param {string} filename - The source filename for fallback naming
138
+ * @returns {{name: string, description: string, body: string, filename: string}}
139
+ */
140
+ export function parseSubagentDefinition(content, filename = '') {
141
+ const metadata = parseSubagentFrontmatter(content);
142
+ const { body } = extractSubagentSections(content);
143
+
144
+ return {
145
+ filename,
146
+ name: metadata.name || humanizeAgentName(filename),
147
+ description: metadata.description || '',
148
+ body,
149
+ };
150
+ }
151
+
105
152
  /**
106
153
  * Get the subagents repository clone URL
107
154
  * @returns {string}
@@ -110,4 +157,4 @@ export function getSubagentsRepoUrl() {
110
157
  return `https://github.com/${SUBAGENTS_REPO_OWNER}/${SUBAGENTS_REPO_NAME}.git`;
111
158
  }
112
159
 
113
- export { SUBAGENTS_REPO_OWNER, SUBAGENTS_REPO_NAME };
160
+ export { SUBAGENTS_REPO_OWNER, SUBAGENTS_REPO_NAME, humanizeAgentName };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supercorks/skills-installer",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "Interactive CLI installer for AI agent skills and subagents",
5
5
  "type": "module",
6
6
  "bin": {