Merge main and integrate command_prefix support into factory

Resolve conflict from codex.js (deleted in our branch, modified on main
for command_prefix). The command_prefix feature from main is preserved
in factory.js via PROVIDER_PLACEHOLDERS lookup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-03-24 15:19:13 -07:00
co-authored by Claude Opus 4.6
4 changed files with 53 additions and 23 deletions
+5 -4
View File
@@ -1,5 +1,5 @@
import path from 'path';
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences } from '../utils.js';
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences, PROVIDER_PLACEHOLDERS } from '../utils.js';
/**
* Map from frontmatter field name to extraction spec.
@@ -86,8 +86,9 @@ export function createTransformer(config) {
const frontmatter = generateYamlFrontmatter(frontmatterObj);
// Build body
let skillBody = replacePlaceholders(skill.body, provider, commandNames);
if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames);
const cmdPrefix = (PROVIDER_PLACEHOLDERS[provider] || {}).command_prefix || '/';
let skillBody = replacePlaceholders(skill.body, provider, commandNames, allSkillNames);
if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames, cmdPrefix);
if (bodyTransform) skillBody = bodyTransform(skillBody, skill);
const content = `${frontmatter}\n\n${skillBody}`;
@@ -98,7 +99,7 @@ export function createTransformer(config) {
const refDir = path.join(skillDir, 'reference');
ensureDir(refDir);
for (const ref of skill.references) {
const refContent = replacePlaceholders(ref.content, provider);
const refContent = replacePlaceholders(ref.content, provider, [], allSkillNames);
writeFile(path.join(refDir, `${ref.name}.md`), refContent);
refCount++;
}
+42 -13
View File
@@ -271,42 +271,50 @@ export const PROVIDER_PLACEHOLDERS = {
'claude-code': {
model: 'Claude',
config_file: 'CLAUDE.md',
ask_instruction: 'STOP and call the AskUserQuestion tool to clarify.'
ask_instruction: 'STOP and call the AskUserQuestion tool to clarify.',
command_prefix: '/'
},
'cursor': {
model: 'the model',
config_file: '.cursorrules',
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
ask_instruction: 'ask the user directly to clarify what you cannot infer.',
command_prefix: '/'
},
'gemini': {
model: 'Gemini',
config_file: 'GEMINI.md',
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
ask_instruction: 'ask the user directly to clarify what you cannot infer.',
command_prefix: '/'
},
'codex': {
model: 'GPT',
config_file: 'AGENTS.md',
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
ask_instruction: 'ask the user directly to clarify what you cannot infer.',
command_prefix: '$'
},
'agents': {
model: 'the model',
config_file: '.github/copilot-instructions.md',
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
ask_instruction: 'ask the user directly to clarify what you cannot infer.',
command_prefix: '/'
},
'kiro': {
model: 'Claude',
config_file: '.kiro/settings.json',
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
ask_instruction: 'ask the user directly to clarify what you cannot infer.',
command_prefix: '/'
},
opencode: {
model: 'Claude',
config_file: 'AGENTS.md',
ask_instruction: 'STOP and call the `question` tool to clarify.',
command_prefix: '/'
},
'pi': {
model: 'the model',
config_file: 'AGENTS.md',
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
ask_instruction: 'ask the user directly to clarify what you cannot infer.',
command_prefix: '/'
}
};
@@ -320,8 +328,9 @@ export const PROVIDER_PLACEHOLDERS = {
* @param {string} content - The skill body text
* @param {string} prefix - The prefix to add (e.g., 'i-')
* @param {string[]} skillNames - Array of all skill names
* @param {string} commandPrefix - The command invocation prefix (e.g., '/' or '$')
*/
export function prefixSkillReferences(content, prefix, skillNames) {
export function prefixSkillReferences(content, prefix, skillNames, commandPrefix = '/') {
if (!prefix || !skillNames || skillNames.length === 0) return content;
let result = content;
@@ -331,8 +340,12 @@ export function prefixSkillReferences(content, prefix, skillNames) {
for (const name of sorted) {
const prefixed = `${prefix}${name}`;
// Replace `/skillname` references (command invocations)
result = result.replace(new RegExp(`\\/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'), `/${prefix}`);
// Replace command invocations (e.g., `/skillname` or `$skillname`) with prefixed versions
const escapedPrefix = escapeRegex(commandPrefix);
result = result.replace(
new RegExp(`${escapedPrefix}(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
`${commandPrefix}${prefix}`
);
// Replace `the skillname skill` references
result = result.replace(
@@ -350,18 +363,34 @@ function escapeRegex(str) {
const EXCLUDED_FROM_SUGGESTIONS = new Set(['teach-impeccable', 'i-teach-impeccable']);
export function replacePlaceholders(content, provider, commandNames = []) {
export function replacePlaceholders(content, provider, commandNames = [], allSkillNames = []) {
const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS['cursor'];
const cmdPrefix = placeholders.command_prefix || '/';
const commandList = commandNames
.filter(n => !EXCLUDED_FROM_SUGGESTIONS.has(n))
.map(n => `/${n}`)
.map(n => `${cmdPrefix}${n}`)
.join(', ');
return content
let result = content
.replace(/\{\{model\}\}/g, placeholders.model)
.replace(/\{\{config_file\}\}/g, placeholders.config_file)
.replace(/\{\{ask_instruction\}\}/g, placeholders.ask_instruction)
.replace(/\{\{command_prefix\}\}/g, cmdPrefix)
.replace(/\{\{available_commands\}\}/g, commandList);
// Replace `/skillname` invocations with the correct command prefix for this provider
// (e.g., `/normalize` → `$normalize` for Codex)
if (cmdPrefix !== '/' && allSkillNames.length > 0) {
const sorted = [...allSkillNames].sort((a, b) => b.length - a.length);
for (const name of sorted) {
result = result.replace(
new RegExp(`\\/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
cmdPrefix
);
}
}
return result;
}
/**