Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions commands/coldbox/ai/skills/create.cfc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Create a custom skill template
* Scaffolds a new skill in .agents/skills/{name}/
* Scaffolds a new skill in .agents/skills-custom/{name}/
*
* Examples:
* coldbox ai skills create api-development
Expand Down Expand Up @@ -44,7 +44,7 @@ component extends="coldbox-cli.models.BaseAICommand" {
printInfo( "Creating custom skill: #arguments.name# (#uCase( language )#)" )

// Check if already exists
var skillPath = skillManager.getSkillsDirectory( arguments.directory ) & "/#arguments.name#/SKILL.md"
var skillPath = skillManager.getCustomSkillsDirectory( arguments.directory ) & "/#arguments.name#/SKILL.md"
if ( fileExists( skillPath ) ) {
printError( "Skill '#arguments.name#' already exists at:" )
printError( " #skillPath#" )
Expand Down
18 changes: 11 additions & 7 deletions commands/coldbox/ai/skills/list.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +37,32 @@ component extends="coldbox-cli.models.BaseAICommand" {
printSuccess( "All skills are up to date." )
return
}
info.skills = info.skills.filter( ( s ) => staleNames.find( s.name ) > 0 )
info.skills = info.skills.filter( ( s ) => staleNames.find( s.name ) > 0 )
info.customSkills = [] // custom skills have no remote SHA, cannot be outdated
printWarn( "#staleNames.len()# skill(s) have updates available:" )
print.line()
}

if ( info.skills.isEmpty() ) {
if ( info.skills.isEmpty() && info.customSkills.isEmpty() ) {
printWarn( "No skills installed. Run 'coldbox ai skills install --list' to browse the registry." )
return
}

// Group by owner/repo (custom skills get bucket "custom")
// Group by owner/repo (custom skills in their own bucket from customSkills manifest section)
var groups = {}
info.skills.each( ( skill ) => {
var bucket = ( skill.type ?: "" ) == "custom"
? "custom"
: ( ( skill.owner ?: "" ) != "" ? "#skill.owner#/#skill.repo#" : "unknown" )
var bucket = ( skill.owner ?: "" ) != "" ? "#skill.owner#/#skill.repo#" : "unknown"
if ( !groups.keyExists( bucket ) ) {
groups[ bucket ] = []
}
groups[ bucket ].append( skill )
} )

// Add custom skills from manifest.customSkills
if ( info.customSkills.len() ) {
groups[ "custom" ] = info.customSkills
}

// Sort groups: custom last, then alphabetical
var groupKeys = groups
.keyArray()
Expand Down Expand Up @@ -105,7 +109,7 @@ component extends="coldbox-cli.models.BaseAICommand" {

// Summary
print.line()
printInfo( "Total: #info.skills.len()# skill(s) installed" )
printInfo( "Total: #(info.skills.len() + info.customSkills.len())# skill(s) installed" )
print.line()

if ( !outdated ) {
Expand Down
4 changes: 2 additions & 2 deletions commands/coldbox/ai/skills/override.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ component extends="coldbox-cli.models.BaseAICommand" {

var skill = existing[ 1 ]

// Check if override already exists (flat path)
var overridePath = skillManager.getSkillsDirectory( arguments.directory ) & "/#arguments.name#/SKILL.md"
// Check if override already exists (in skills-custom/)
var overridePath = skillManager.getCustomSkillsDirectory( arguments.directory ) & "/#arguments.name#/SKILL.md"
if ( fileExists( overridePath ) ) {
printWarn( "Skill '#arguments.name#' already exists at:" )
printWarn( " #overridePath#" )
Expand Down
2 changes: 1 addition & 1 deletion commands/coldbox/ai/skills/remove.cfc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Remove a skill from the project by name.
* Skills are stored in the flat .agents/skills/{name}/ directory.
* Checks both .agents/skills/{name}/ and .agents/skills-custom/{name}/ directories.
*
* Examples:
* coldbox ai skills remove boxlang-syntax
Expand Down
30 changes: 18 additions & 12 deletions models/AIService.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ component singleton {
"templateType" : templateType,
"guidelines" : [],
"skills" : [],
"customSkills" : [],
"agents" : listToArray( arguments.agents ),
"mcpServers" : {
"core" : [],
Expand Down Expand Up @@ -258,6 +259,7 @@ component singleton {
"lastSync" : manifest.lastSync ?: "never",
"guidelines" : manifest.guidelines ?: [],
"skills" : manifest.skills ?: [],
"customSkills" : manifest.customSkills ?: [],
"agents" : manifest.agents ?: [],
"mcpServers" : manifest.mcpServers ?: {
"core" : [],
Expand Down Expand Up @@ -439,7 +441,8 @@ component singleton {
"#aiDir#/guidelines",
"#aiDir#/guidelines/core",
"#aiDir#/guidelines/custom",
"#aiDir#/skills"
"#aiDir#/skills",
"#aiDir#/skills-custom"
];

dirs.each( ( dir ) => {
Expand Down Expand Up @@ -474,10 +477,10 @@ component singleton {
"onDemandSize" : 0
},
"skills" : {
"total" : info.skills.len(),
"total" : info.skills.len() + info.customSkills.len(),
"core" : 0,
"module" : 0,
"custom" : 0,
"custom" : info.customSkills.len(),
"override" : 0,
"totalSize" : 0,
"avgSize" : 0
Expand Down Expand Up @@ -565,20 +568,23 @@ component singleton {
stats.skills.override++;
} else if ( source == "core" ) {
stats.skills.core++;
} else if ( source == "custom" || type == "custom" ) {
stats.skills.custom++;
} else {
stats.skills.module++;
}
} );

// Skills size (all on-demand)
var skillsDir = aiDir & "/skills";
// Skills size (all on-demand) — includes both skills/ and skills-custom/
var skillsDir = aiDir & "/skills";
var customSkillsDir = aiDir & "/skills-custom";
var skillsSize = 0;
if ( directoryExists( skillsDir ) ) {
var skillsSize = calculateDirectorySize( skillsDir );
stats.skills.totalSize = skillsSize;
stats.skills.avgSize = stats.skills.total > 0 ? int( skillsSize / stats.skills.total ) : 0;
skillsSize += calculateDirectorySize( skillsDir );
}
if ( directoryExists( customSkillsDir ) ) {
skillsSize += calculateDirectorySize( customSkillsDir );
}
stats.skills.totalSize = skillsSize;
stats.skills.avgSize = stats.skills.total > 0 ? int( skillsSize / stats.skills.total ) : 0;

// Count MCP servers
var mcpServers = manifest.mcpServers ?: {
Expand Down Expand Up @@ -607,10 +613,10 @@ component singleton {
// Inlined guidelines (part of base context, shown separately for clarity)
stats.contextEstimate.inlinedKB = int( stats.guidelines.inlinedSize / 1024 );
// On-demand resources (not in base context, but available)
stats.contextEstimate.onDemandKB = int( ( stats.guidelines.onDemandSize + stats.skills.totalSize ) / 1024 );
stats.contextEstimate.onDemandKB = int( ( stats.guidelines.onDemandSize + skillsSize ) / 1024 );
// Total available if all resources were loaded
stats.contextEstimate.totalAvailableKB = int(
( stats.agents.filesSize + stats.guidelines.onDemandSize + stats.skills.totalSize ) / 1024
( stats.agents.filesSize + stats.guidelines.onDemandSize + skillsSize ) / 1024
);

return stats;
Expand Down
13 changes: 8 additions & 5 deletions models/AgentRegistry.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,10 @@ component singleton {
var aiService = variables.wirebox.getInstance( "AIService@coldbox-cli" )
var manifest = aiService.loadManifest( arguments.directory )

if ( !structKeyExists( manifest, "skills" ) || !manifest.skills.len() ) {
var hasSkills = structKeyExists( manifest, "skills" ) && manifest.skills.len()
var hasCustomSkills = structKeyExists( manifest, "customSkills" ) && manifest.customSkills.len()

if ( !hasSkills && !hasCustomSkills ) {
return "No skills installed yet. Run 'coldbox ai install' to get started."
}

Expand All @@ -726,9 +729,9 @@ component singleton {
}

var content = []
var coreSkills = manifest.skills.filter( ( s ) => s.source == "core" )
var moduleSkills = manifest.skills.filter( ( s ) => s.source != "core" && s.source != "custom" )
var customSkills = manifest.skills.filter( ( s ) => s.source == "custom" )
var coreSkills = hasSkills ? manifest.skills.filter( ( s ) => s.source == "core" ) : []
var moduleSkills = hasSkills ? manifest.skills.filter( ( s ) => s.source != "core" && s.source != "custom" ) : []
var customSkills = manifest.customSkills ?: []

// Helper: group skills by prefix and append formatted output to content
var appendGroupedSkills = ( skills, sectionLabel ) => {
Expand Down Expand Up @@ -772,7 +775,7 @@ component singleton {
appendGroupedSkills( moduleSkills, "Module Skills" )
appendGroupedSkills( customSkills, "Custom Skills" )

content.append( "**To load a skill:** Use `read_file` on `.ai/skills/{skill-name}/SKILL.md` (e.g., `.ai/skills/coldbox-handler-development/SKILL.md`)." )
content.append( "**To load a skill:** Use `read_file` on `.agents/skills/{skill-name}/SKILL.md` (e.g., `.agents/skills/coldbox-handler-development/SKILL.md`) for core skills, or `.agents/skills-custom/{skill-name}/SKILL.md` for custom project skills." )

return content.toList( chr( 10 ) )
}
Expand Down
Loading