@supercorks/skills-installer 1.11.1 → 1.13.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 +24 -12
- package/bin/install.js +168 -40
- package/lib/codex-agents.js +165 -0
- package/lib/install-targets.js +99 -0
- package/lib/prompts.js +70 -50
- package/lib/subagents.js +58 -11
- package/package.json +1 -1
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,25 +18,35 @@ 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
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- `.agents/
|
|
27
|
-
- `.claude/
|
|
21
|
+
2. **Choose installation path(s)** - Select one or more locations where resources should be installed. Global locations are shown before local locations. The installer labels each option with harness, scope, and available count, for example `~/.agents/skills/ (copilot/codex | global | 24 skills)`.
|
|
22
|
+
|
|
23
|
+
Skills:
|
|
24
|
+
- `~/.agents/skills/` (copilot/codex | global)
|
|
25
|
+
- `~/.claude/skills/` (claude | global)
|
|
26
|
+
- `.agents/skills/` (copilot/codex | local)
|
|
27
|
+
- `.claude/skills/` (claude | local)
|
|
28
|
+
|
|
29
|
+
Agents:
|
|
30
|
+
- `~/.agents/agents/` (copilot | global)
|
|
31
|
+
- `~/.claude/agents/` (claude | global)
|
|
32
|
+
- `~/.codex/agents/` (codex | global, installed as converted TOML agents)
|
|
33
|
+
- `.agents/agents/` (copilot | local)
|
|
34
|
+
- `.claude/agents/` (claude | local)
|
|
35
|
+
- `.codex/agents/` (codex | local, installed as converted TOML agents)
|
|
28
36
|
- Custom path of your choice
|
|
29
37
|
|
|
30
38
|
3. **Gitignore option** - If launched from inside a git repository, optionally add the installation path to `.gitignore`
|
|
31
39
|
|
|
32
|
-
4. **Select skills/subagents** - Interactive checkbox to pick what to install:
|
|
40
|
+
4. **Select skills/subagents** - Interactive checkbox to pick what to install. If multiple locations are selected, the installer asks once and applies the same selection to every selected location:
|
|
33
41
|
- Use `↑`/`↓` to navigate
|
|
34
42
|
- Use `SPACE` to toggle selection
|
|
35
43
|
- Use `→` to expand and lazy-load descriptions
|
|
36
44
|
- Use `A` to toggle all
|
|
37
45
|
- Press `ENTER` to confirm
|
|
38
46
|
|
|
39
|
-
5. **
|
|
47
|
+
5. **Install backend**
|
|
48
|
+
- Skills and Markdown-based agents use Git sparse-checkout for minimal download while preserving full git functionality.
|
|
49
|
+
- Codex agents are generated as TOML files from the source Markdown agent definitions.
|
|
40
50
|
|
|
41
51
|
## Installed repositories
|
|
42
52
|
|
|
@@ -48,6 +58,8 @@ npx @supercorks/skills-installer install
|
|
|
48
58
|
- **Minimal download** - Uses `git clone --filter=blob:none` for efficient cloning
|
|
49
59
|
- **Push capable** - The sparse clone preserves the full git history, allowing you to commit and push changes
|
|
50
60
|
- **Auto-discovery** - Fetches the latest skill list from the repository
|
|
61
|
+
- **Global and local targets** - Offers documented project/user locations for Copilot, Codex, and Claude where the resource format is compatible, with shared generic `~/.agents/skills/` and `.agents/skills/` targets for Copilot/Codex skills
|
|
62
|
+
- **Codex agent conversion** - Converts Markdown subagents into Codex TOML custom agents for `.codex/agents/` targets
|
|
51
63
|
- **Recursive directory creation** - Custom paths are created automatically
|
|
52
64
|
|
|
53
65
|
## Requirements
|
|
@@ -60,7 +72,7 @@ npx @supercorks/skills-installer install
|
|
|
60
72
|
Since the installation uses a sparse git checkout, you can pull updates:
|
|
61
73
|
|
|
62
74
|
```bash
|
|
63
|
-
cd .
|
|
75
|
+
cd .agents/skills # or wherever you installed
|
|
64
76
|
git pull
|
|
65
77
|
```
|
|
66
78
|
|
|
@@ -69,7 +81,7 @@ git pull
|
|
|
69
81
|
You can add more skills to an existing installation:
|
|
70
82
|
|
|
71
83
|
```bash
|
|
72
|
-
cd .
|
|
84
|
+
cd .agents/skills
|
|
73
85
|
git sparse-checkout add new-skill-name
|
|
74
86
|
```
|
|
75
87
|
|
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 =
|
|
46
|
-
const AGENT_PATHS =
|
|
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();
|
|
@@ -55,6 +57,14 @@ function isHomePath(path) {
|
|
|
55
57
|
return path === '~' || path.startsWith('~/');
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
function uniqueItems(items) {
|
|
61
|
+
return Array.from(new Set(items));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function unionSets(sets) {
|
|
65
|
+
return new Set(sets.flatMap(set => Array.from(set)));
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
/**
|
|
59
69
|
* Detect existing skill installations in common paths
|
|
60
70
|
* @returns {Promise<Array<{path: string, skillCount: number, skills: string[]}>>}
|
|
@@ -92,6 +102,24 @@ async function detectExistingAgentInstallations() {
|
|
|
92
102
|
|
|
93
103
|
for (const path of AGENT_PATHS) {
|
|
94
104
|
const absolutePath = resolveInstallPath(path);
|
|
105
|
+
const installMode = getAgentInstallMode(path);
|
|
106
|
+
|
|
107
|
+
if (installMode === 'codex-toml') {
|
|
108
|
+
try {
|
|
109
|
+
const agents = await listInstalledCodexAgents(absolutePath);
|
|
110
|
+
if (agents.length > 0) {
|
|
111
|
+
installations.push({
|
|
112
|
+
path,
|
|
113
|
+
agentCount: agents.length,
|
|
114
|
+
agents
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Ignore errors reading existing installations
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
95
123
|
const gitDir = join(absolutePath, '.git');
|
|
96
124
|
|
|
97
125
|
if (existsSync(gitDir)) {
|
|
@@ -220,24 +248,41 @@ async function runSkillsInstall() {
|
|
|
220
248
|
const existingInstalls = await detectExistingSkillInstallations();
|
|
221
249
|
|
|
222
250
|
// Ask where to install (showing existing installations if any)
|
|
223
|
-
const installTargets = await promptInstallPath(existingInstalls);
|
|
251
|
+
const installTargets = await promptInstallPath(existingInstalls, skills.length);
|
|
224
252
|
|
|
225
|
-
|
|
226
|
-
|
|
253
|
+
const targetContexts = [];
|
|
254
|
+
for (const [index, target] of installTargets.entries()) {
|
|
227
255
|
if (installTargets.length > 1) {
|
|
228
|
-
console.log(`\n📍
|
|
256
|
+
console.log(`\n📍 Preparing skills target ${index + 1}/${installTargets.length}: ${target.path}`);
|
|
257
|
+
}
|
|
258
|
+
targetContexts.push(await prepareSkillsInstallTarget(existingInstalls, target));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const installedSkills = uniqueItems(targetContexts.flatMap(context => context.installedSkills));
|
|
262
|
+
const skillsNeedingUpdate = unionSets(targetContexts.map(context => context.skillsNeedingUpdate));
|
|
263
|
+
|
|
264
|
+
const selectedSkills = await promptSkillSelection(
|
|
265
|
+
skills,
|
|
266
|
+
installedSkills,
|
|
267
|
+
skillsNeedingUpdate,
|
|
268
|
+
(skillFolder) => fetchSkillMetadata(skillFolder)
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < targetContexts.length; i++) {
|
|
272
|
+
if (targetContexts.length > 1) {
|
|
273
|
+
console.log(`\n📍 Skills target ${i + 1}/${targetContexts.length}: ${targetContexts[i].installPath}`);
|
|
229
274
|
}
|
|
230
|
-
await runSkillsInstallForTarget(skills,
|
|
275
|
+
await runSkillsInstallForTarget(skills, targetContexts[i], selectedSkills);
|
|
231
276
|
}
|
|
232
277
|
}
|
|
233
278
|
|
|
234
279
|
/**
|
|
235
|
-
*
|
|
236
|
-
* @param {Array<{name: string, description: string, folder: string}>} skills
|
|
280
|
+
* Prepare a specific skills target for installation/update.
|
|
237
281
|
* @param {Array<{path: string, skillCount: number, skills: string[]}>} existingInstalls
|
|
238
282
|
* @param {{path: string, isExisting: boolean}} target
|
|
283
|
+
* @returns {Promise<object>}
|
|
239
284
|
*/
|
|
240
|
-
async function
|
|
285
|
+
async function prepareSkillsInstallTarget(existingInstalls, target) {
|
|
241
286
|
const { path: installPath, isExisting } = target;
|
|
242
287
|
const absoluteInstallPath = resolveInstallPath(installPath);
|
|
243
288
|
const gitDir = join(absoluteInstallPath, '.git');
|
|
@@ -291,13 +336,33 @@ async function runSkillsInstallForTarget(skills, existingInstalls, target) {
|
|
|
291
336
|
shouldGitignore = await promptGitignore(installPath);
|
|
292
337
|
}
|
|
293
338
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
339
|
+
return {
|
|
340
|
+
...target,
|
|
341
|
+
installPath,
|
|
342
|
+
absoluteInstallPath,
|
|
297
343
|
installedSkills,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
344
|
+
isManageMode,
|
|
345
|
+
shouldGitignore,
|
|
346
|
+
gitignorePath,
|
|
347
|
+
skillsNeedingUpdate
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Install/update skills for a specific target path
|
|
353
|
+
* @param {Array<{name: string, description: string, folder: string}>} skills
|
|
354
|
+
* @param {object} targetContext
|
|
355
|
+
* @param {string[]} selectedSkills
|
|
356
|
+
*/
|
|
357
|
+
async function runSkillsInstallForTarget(skills, targetContext, selectedSkills) {
|
|
358
|
+
const {
|
|
359
|
+
installPath,
|
|
360
|
+
absoluteInstallPath,
|
|
361
|
+
installedSkills,
|
|
362
|
+
isManageMode,
|
|
363
|
+
shouldGitignore,
|
|
364
|
+
gitignorePath
|
|
365
|
+
} = targetContext;
|
|
301
366
|
|
|
302
367
|
// Perform installation or update
|
|
303
368
|
console.log('');
|
|
@@ -380,35 +445,53 @@ async function runSubagentsInstall() {
|
|
|
380
445
|
const existingInstalls = await detectExistingAgentInstallations();
|
|
381
446
|
|
|
382
447
|
// Ask where to install (showing existing installations if any)
|
|
383
|
-
const installTargets = await promptAgentInstallPath(existingInstalls);
|
|
448
|
+
const installTargets = await promptAgentInstallPath(existingInstalls, subagents.length);
|
|
384
449
|
|
|
385
|
-
|
|
386
|
-
|
|
450
|
+
const targetContexts = [];
|
|
451
|
+
for (const [index, target] of installTargets.entries()) {
|
|
387
452
|
if (installTargets.length > 1) {
|
|
388
|
-
console.log(`\n📍
|
|
453
|
+
console.log(`\n📍 Preparing subagents target ${index + 1}/${installTargets.length}: ${target.path}`);
|
|
454
|
+
}
|
|
455
|
+
targetContexts.push(await prepareSubagentsInstallTarget(existingInstalls, target));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const installedAgents = uniqueItems(targetContexts.flatMap(context => context.installedAgents));
|
|
459
|
+
const subagentsNeedingUpdate = unionSets(targetContexts.map(context => context.subagentsNeedingUpdate));
|
|
460
|
+
|
|
461
|
+
const selectedAgents = await promptSubagentSelection(
|
|
462
|
+
subagents,
|
|
463
|
+
installedAgents,
|
|
464
|
+
subagentsNeedingUpdate,
|
|
465
|
+
(filename) => fetchSubagentMetadata(filename)
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
for (let i = 0; i < targetContexts.length; i++) {
|
|
469
|
+
if (targetContexts.length > 1) {
|
|
470
|
+
console.log(`\n📍 Subagents target ${i + 1}/${targetContexts.length}: ${targetContexts[i].installPath}`);
|
|
389
471
|
}
|
|
390
|
-
await runSubagentsInstallForTarget(subagents,
|
|
472
|
+
await runSubagentsInstallForTarget(subagents, targetContexts[i], selectedAgents);
|
|
391
473
|
}
|
|
392
474
|
}
|
|
393
475
|
|
|
394
476
|
/**
|
|
395
|
-
*
|
|
396
|
-
* @param {Array<{name: string, description: string, filename: string}>} subagents
|
|
477
|
+
* Prepare a specific subagent target for installation/update.
|
|
397
478
|
* @param {Array<{path: string, agentCount: number, agents: string[]}>} existingInstalls
|
|
398
479
|
* @param {{path: string, isExisting: boolean}} target
|
|
480
|
+
* @returns {Promise<object>}
|
|
399
481
|
*/
|
|
400
|
-
async function
|
|
482
|
+
async function prepareSubagentsInstallTarget(existingInstalls, target) {
|
|
401
483
|
const { path: installPath, isExisting } = target;
|
|
402
484
|
const absoluteInstallPath = resolveInstallPath(installPath);
|
|
485
|
+
const installMode = getAgentInstallMode(installPath);
|
|
403
486
|
const gitDir = join(absoluteInstallPath, '.git');
|
|
404
|
-
const hasExistingRepo = existsSync(gitDir);
|
|
487
|
+
const hasExistingRepo = installMode === 'sparse-git' && existsSync(gitDir);
|
|
405
488
|
|
|
406
489
|
// Get currently installed subagents if managing existing installation
|
|
407
490
|
let installedAgents = [];
|
|
408
491
|
if (isExisting) {
|
|
409
492
|
const existingInstall = existingInstalls.find(i => i.path === installPath);
|
|
410
493
|
installedAgents = existingInstall?.agents || [];
|
|
411
|
-
} else {
|
|
494
|
+
} else if (installMode === 'sparse-git') {
|
|
412
495
|
// Check if manually entered path has an existing installation
|
|
413
496
|
if (hasExistingRepo) {
|
|
414
497
|
try {
|
|
@@ -418,16 +501,27 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
|
|
|
418
501
|
// Prompt will default to selecting all subagents.
|
|
419
502
|
}
|
|
420
503
|
}
|
|
504
|
+
} else {
|
|
505
|
+
try {
|
|
506
|
+
installedAgents = await listInstalledCodexAgents(absoluteInstallPath);
|
|
507
|
+
} catch {
|
|
508
|
+
// Ignore detection failures for custom Codex agent paths.
|
|
509
|
+
}
|
|
421
510
|
}
|
|
422
511
|
|
|
423
|
-
const isManageMode =
|
|
512
|
+
const isManageMode = installMode === 'sparse-git'
|
|
513
|
+
? (isExisting || hasExistingRepo || installedAgents.length > 0)
|
|
514
|
+
: installedAgents.length > 0;
|
|
424
515
|
|
|
425
516
|
// Check for updates if in manage mode
|
|
426
517
|
let subagentsNeedingUpdate = new Set();
|
|
427
518
|
if (isManageMode) {
|
|
428
519
|
const updateSpinner = showSpinner('Checking for available updates...');
|
|
429
520
|
try {
|
|
430
|
-
subagentsNeedingUpdate =
|
|
521
|
+
subagentsNeedingUpdate = installMode === 'sparse-git'
|
|
522
|
+
? await checkSubagentsForUpdates(absoluteInstallPath, installedAgents)
|
|
523
|
+
: await checkCodexAgentUpdates(absoluteInstallPath, installedAgents);
|
|
524
|
+
|
|
431
525
|
if (subagentsNeedingUpdate.size > 0) {
|
|
432
526
|
updateSpinner.stop(`✅ Found ${subagentsNeedingUpdate.size} subagent${subagentsNeedingUpdate.size !== 1 ? 's' : ''} with updates available`);
|
|
433
527
|
} else {
|
|
@@ -451,13 +545,35 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
|
|
|
451
545
|
shouldGitignore = await promptGitignore(installPath);
|
|
452
546
|
}
|
|
453
547
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
548
|
+
return {
|
|
549
|
+
...target,
|
|
550
|
+
installPath,
|
|
551
|
+
absoluteInstallPath,
|
|
552
|
+
installMode,
|
|
457
553
|
installedAgents,
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
554
|
+
isManageMode,
|
|
555
|
+
shouldGitignore,
|
|
556
|
+
gitignorePath,
|
|
557
|
+
subagentsNeedingUpdate
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Install/update subagents for a specific target path
|
|
563
|
+
* @param {Array<{name: string, description: string, filename: string}>} subagents
|
|
564
|
+
* @param {object} targetContext
|
|
565
|
+
* @param {string[]} selectedAgents
|
|
566
|
+
*/
|
|
567
|
+
async function runSubagentsInstallForTarget(subagents, targetContext, selectedAgents) {
|
|
568
|
+
const {
|
|
569
|
+
installPath,
|
|
570
|
+
absoluteInstallPath,
|
|
571
|
+
installMode,
|
|
572
|
+
installedAgents,
|
|
573
|
+
isManageMode,
|
|
574
|
+
shouldGitignore,
|
|
575
|
+
gitignorePath
|
|
576
|
+
} = targetContext;
|
|
461
577
|
|
|
462
578
|
// Perform installation or update
|
|
463
579
|
console.log('');
|
|
@@ -475,9 +591,15 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
|
|
|
475
591
|
const updateSpinner = showSpinner('Updating subagents installation...');
|
|
476
592
|
|
|
477
593
|
try {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
594
|
+
if (installMode === 'sparse-git') {
|
|
595
|
+
await updateSubagentsSparseCheckout(absoluteInstallPath, selectedAgents, (message) => {
|
|
596
|
+
updateSpinner.stop(` ${message}`);
|
|
597
|
+
});
|
|
598
|
+
} else {
|
|
599
|
+
await syncCodexAgents(absoluteInstallPath, selectedAgents, (message) => {
|
|
600
|
+
updateSpinner.stop(` ${message}`);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
481
603
|
} catch (error) {
|
|
482
604
|
updateSpinner.stop('❌ Update failed');
|
|
483
605
|
showError(error.message);
|
|
@@ -490,9 +612,15 @@ async function runSubagentsInstallForTarget(subagents, existingInstalls, target)
|
|
|
490
612
|
const installSpinner = showSpinner('Installing selected subagents...');
|
|
491
613
|
|
|
492
614
|
try {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
615
|
+
if (installMode === 'sparse-git') {
|
|
616
|
+
await sparseCloneSubagents(installPath, selectedAgents, (message) => {
|
|
617
|
+
installSpinner.stop(` ${message}`);
|
|
618
|
+
});
|
|
619
|
+
} else {
|
|
620
|
+
await syncCodexAgents(absoluteInstallPath, selectedAgents, (message) => {
|
|
621
|
+
installSpinner.stop(` ${message}`);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
496
624
|
} catch (error) {
|
|
497
625
|
installSpinner.stop('❌ Installation failed');
|
|
498
626
|
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,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported install targets for skills and agents.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SKILL_INSTALL_TARGETS = [
|
|
6
|
+
{ path: '~/.agents/skills/', harness: 'copilot/codex', scope: 'global' },
|
|
7
|
+
{ path: '~/.claude/skills/', harness: 'claude', scope: 'global' },
|
|
8
|
+
{ path: '.agents/skills/', harness: 'copilot/codex', scope: 'local' },
|
|
9
|
+
{ path: '.claude/skills/', harness: 'claude', scope: 'local' }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export const LEGACY_SKILL_INSTALL_TARGETS = [
|
|
13
|
+
{
|
|
14
|
+
path: '.github/skills/',
|
|
15
|
+
harness: 'copilot',
|
|
16
|
+
scope: 'legacy local'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: '~/.copilot/skills/',
|
|
20
|
+
harness: 'copilot',
|
|
21
|
+
scope: 'legacy global'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: '~/.codex/skills/',
|
|
25
|
+
harness: 'codex',
|
|
26
|
+
scope: 'legacy global'
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const AGENT_INSTALL_TARGETS = [
|
|
31
|
+
{ path: '~/.agents/agents/', harness: 'copilot', scope: 'global', installMode: 'sparse-git' },
|
|
32
|
+
{ path: '~/.claude/agents/', harness: 'claude', scope: 'global', installMode: 'sparse-git' },
|
|
33
|
+
{ path: '~/.codex/agents/', harness: 'codex', scope: 'global', installMode: 'codex-toml' },
|
|
34
|
+
{ path: '.agents/agents/', harness: 'copilot', scope: 'local', installMode: 'sparse-git' },
|
|
35
|
+
{ path: '.claude/agents/', harness: 'claude', scope: 'local', installMode: 'sparse-git' },
|
|
36
|
+
{ path: '.codex/agents/', harness: 'codex', scope: 'local', installMode: 'codex-toml' }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export const LEGACY_AGENT_INSTALL_TARGETS = [
|
|
40
|
+
{
|
|
41
|
+
path: '.github/agents/',
|
|
42
|
+
harness: 'copilot',
|
|
43
|
+
scope: 'legacy local'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: '~/.copilot/agents/',
|
|
47
|
+
harness: 'copilot',
|
|
48
|
+
scope: 'legacy global'
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export function allSkillDetectionTargets() {
|
|
53
|
+
return [...SKILL_INSTALL_TARGETS, ...LEGACY_SKILL_INSTALL_TARGETS];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function allAgentDetectionTargets() {
|
|
57
|
+
return [...AGENT_INSTALL_TARGETS, ...LEGACY_AGENT_INSTALL_TARGETS];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function orderTargetsGlobalFirst(targets) {
|
|
61
|
+
return [...targets].sort((left, right) => {
|
|
62
|
+
const leftGlobal = left.scope.includes('global') ? 0 : 1;
|
|
63
|
+
const rightGlobal = right.scope.includes('global') ? 0 : 1;
|
|
64
|
+
|
|
65
|
+
if (leftGlobal !== rightGlobal) {
|
|
66
|
+
return leftGlobal - rightGlobal;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return targets.indexOf(left) - targets.indexOf(right);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getAgentInstallMode(path) {
|
|
74
|
+
const exactTarget = getTargetByPath(AGENT_INSTALL_TARGETS, path);
|
|
75
|
+
if (exactTarget?.installMode) {
|
|
76
|
+
return exactTarget.installMode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const normalizedPath = path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
80
|
+
if (normalizedPath.endsWith('/.codex/agents') || normalizedPath === '.codex/agents' || normalizedPath === '~/.codex/agents') {
|
|
81
|
+
return 'codex-toml';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return 'sparse-git';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getTargetByPath(targets, path) {
|
|
88
|
+
return targets.find(target => target.path === path);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatTargetLabel(target, count, noun, { installed = false } = {}) {
|
|
92
|
+
const plural = count === 1 ? noun : `${noun}s`;
|
|
93
|
+
const countText = installed ? `${count} ${plural} installed` : `${count} ${plural}`;
|
|
94
|
+
const detail = target
|
|
95
|
+
? `${target.harness} | ${target.scope} | ${countText}`
|
|
96
|
+
: countText;
|
|
97
|
+
|
|
98
|
+
return `${target?.path || ''} (${detail})`;
|
|
99
|
+
}
|
package/lib/prompts.js
CHANGED
|
@@ -4,18 +4,21 @@
|
|
|
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
|
+
orderTargetsGlobalFirst
|
|
15
|
+
} from './install-targets.js';
|
|
7
16
|
|
|
8
17
|
const SKILL_PATH_CHOICES = {
|
|
9
|
-
GITHUB: '.github/skills/',
|
|
10
|
-
CODEX_HOME: '~/.codex/skills/',
|
|
11
|
-
CLAUDE: '.claude/skills/',
|
|
12
18
|
CUSTOM: '__custom__'
|
|
13
19
|
};
|
|
14
20
|
|
|
15
21
|
const AGENT_PATH_CHOICES = {
|
|
16
|
-
GITHUB: '.github/agents/',
|
|
17
|
-
CODEX: '.agents/agents/',
|
|
18
|
-
CLAUDE: '.claude/agents/',
|
|
19
22
|
CUSTOM: '__custom__'
|
|
20
23
|
};
|
|
21
24
|
|
|
@@ -61,36 +64,44 @@ export async function promptInstallType() {
|
|
|
61
64
|
/**
|
|
62
65
|
* Prompt user to select one or more installation paths, showing existing installations
|
|
63
66
|
* @param {Array<{path: string, skillCount: number}>} existingInstalls - Detected existing installations
|
|
67
|
+
* @param {number} availableSkillCount - Number of skills available for new installations
|
|
64
68
|
* @returns {Promise<Array<{path: string, isExisting: boolean}>>} Selected paths and whether each is existing
|
|
65
69
|
*/
|
|
66
|
-
export async function promptInstallPath(existingInstalls = []) {
|
|
70
|
+
export async function promptInstallPath(existingInstalls = [], availableSkillCount = 0) {
|
|
67
71
|
const choices = [];
|
|
68
|
-
const
|
|
72
|
+
const detectionTargets = allSkillDetectionTargets();
|
|
73
|
+
const existingByPath = new Map(existingInstalls.map(install => [install.path, install]));
|
|
74
|
+
const standardPaths = new Set(SKILL_INSTALL_TARGETS.map(target => target.path));
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
orderTargetsGlobalFirst(SKILL_INSTALL_TARGETS).forEach(target => {
|
|
77
|
+
const install = existingByPath.get(target.path);
|
|
78
|
+
choices.push({
|
|
79
|
+
name: install
|
|
80
|
+
? formatTargetLabel(target, install.skillCount, 'skill', { installed: true })
|
|
81
|
+
: formatTargetLabel(target, availableSkillCount, 'skill'),
|
|
82
|
+
value: target.path
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const existingLegacyOrCustom = existingInstalls.filter(install => !standardPaths.has(install.path));
|
|
87
|
+
if (existingLegacyOrCustom.length > 0) {
|
|
88
|
+
choices.push(new inquirer.Separator('── Existing legacy/custom installations ──'));
|
|
89
|
+
orderTargetsGlobalFirst(existingLegacyOrCustom.map(install => getTargetByPath(detectionTargets, install.path) || {
|
|
90
|
+
path: install.path,
|
|
91
|
+
harness: 'custom',
|
|
92
|
+
scope: 'existing'
|
|
93
|
+
})).forEach(target => {
|
|
94
|
+
const install = existingByPath.get(target.path);
|
|
73
95
|
choices.push({
|
|
74
|
-
name:
|
|
96
|
+
name: target
|
|
97
|
+
? formatTargetLabel(target, install.skillCount, 'skill', { installed: true })
|
|
98
|
+
: `${install.path} (${install.skillCount} skill${install.skillCount !== 1 ? 's' : ''} installed)`,
|
|
75
99
|
value: install.path
|
|
76
100
|
});
|
|
77
101
|
});
|
|
78
|
-
choices.push(new inquirer.Separator('── New installation ──'));
|
|
79
102
|
}
|
|
80
|
-
|
|
81
|
-
|
|
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 });
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
103
|
+
|
|
104
|
+
choices.push(new inquirer.Separator('── Other ──'));
|
|
94
105
|
choices.push({ name: 'Custom path...', value: SKILL_PATH_CHOICES.CUSTOM });
|
|
95
106
|
|
|
96
107
|
const { pathChoices } = await inquirer.prompt([
|
|
@@ -116,7 +127,7 @@ export async function promptInstallPath(existingInstalls = []) {
|
|
|
116
127
|
pathChoices
|
|
117
128
|
.filter(path => path !== SKILL_PATH_CHOICES.CUSTOM)
|
|
118
129
|
.forEach(path => {
|
|
119
|
-
selected.push({ path, isExisting:
|
|
130
|
+
selected.push({ path, isExisting: existingByPath.has(path) });
|
|
120
131
|
});
|
|
121
132
|
|
|
122
133
|
if (selectedSet.has(SKILL_PATH_CHOICES.CUSTOM)) {
|
|
@@ -144,36 +155,45 @@ export async function promptInstallPath(existingInstalls = []) {
|
|
|
144
155
|
/**
|
|
145
156
|
* Prompt user to select one or more subagent installation paths
|
|
146
157
|
* @param {Array<{path: string, agentCount: number}>} existingInstalls - Detected existing installations
|
|
158
|
+
* @param {number} availableAgentCount - Number of agents available for new installations
|
|
147
159
|
* @returns {Promise<Array<{path: string, isExisting: boolean}>>} Selected paths and whether each is existing
|
|
148
160
|
*/
|
|
149
|
-
export async function promptAgentInstallPath(existingInstalls = []) {
|
|
161
|
+
export async function promptAgentInstallPath(existingInstalls = [], availableAgentCount = 0) {
|
|
150
162
|
const choices = [];
|
|
151
|
-
const
|
|
163
|
+
const detectionTargets = allAgentDetectionTargets();
|
|
164
|
+
const existingByPath = new Map(existingInstalls.map(install => [install.path, install]));
|
|
165
|
+
const standardPaths = new Set(AGENT_INSTALL_TARGETS.map(target => target.path));
|
|
152
166
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
orderTargetsGlobalFirst(AGENT_INSTALL_TARGETS).forEach(target => {
|
|
168
|
+
const install = existingByPath.get(target.path);
|
|
169
|
+
choices.push({
|
|
170
|
+
name: install
|
|
171
|
+
? formatTargetLabel(target, install.agentCount, 'agent', { installed: true })
|
|
172
|
+
: formatTargetLabel(target, availableAgentCount, 'agent'),
|
|
173
|
+
value: target.path,
|
|
174
|
+
disabled: target.disabledReason
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const existingLegacyOrCustom = existingInstalls.filter(install => !standardPaths.has(install.path));
|
|
179
|
+
if (existingLegacyOrCustom.length > 0) {
|
|
180
|
+
choices.push(new inquirer.Separator('── Existing legacy/custom installations ──'));
|
|
181
|
+
orderTargetsGlobalFirst(existingLegacyOrCustom.map(install => getTargetByPath(detectionTargets, install.path) || {
|
|
182
|
+
path: install.path,
|
|
183
|
+
harness: 'custom',
|
|
184
|
+
scope: 'existing'
|
|
185
|
+
})).forEach(target => {
|
|
186
|
+
const install = existingByPath.get(target.path);
|
|
156
187
|
choices.push({
|
|
157
|
-
name:
|
|
188
|
+
name: target
|
|
189
|
+
? formatTargetLabel(target, install.agentCount, 'agent', { installed: true })
|
|
190
|
+
: `${install.path} (${install.agentCount} agent${install.agentCount !== 1 ? 's' : ''} installed)`,
|
|
158
191
|
value: install.path
|
|
159
192
|
});
|
|
160
193
|
});
|
|
161
|
-
choices.push(new inquirer.Separator('── New installation ──'));
|
|
162
194
|
}
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
195
|
+
|
|
196
|
+
choices.push(new inquirer.Separator('── Other ──'));
|
|
177
197
|
choices.push({ name: 'Custom path...', value: AGENT_PATH_CHOICES.CUSTOM });
|
|
178
198
|
|
|
179
199
|
const { pathChoices } = await inquirer.prompt([
|
|
@@ -199,7 +219,7 @@ export async function promptAgentInstallPath(existingInstalls = []) {
|
|
|
199
219
|
pathChoices
|
|
200
220
|
.filter(path => path !== AGENT_PATH_CHOICES.CUSTOM)
|
|
201
221
|
.forEach(path => {
|
|
202
|
-
selected.push({ path, isExisting:
|
|
222
|
+
selected.push({ path, isExisting: existingByPath.has(path) });
|
|
203
223
|
});
|
|
204
224
|
|
|
205
225
|
if (selectedSet.has(AGENT_PATH_CHOICES.CUSTOM)) {
|
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
|
-
|
|
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
|
-
|
|
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 };
|