mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Merge main into v2.0: consolidate critique skill with scoring, personas, and detection
Merges 54 commits from main including factory-based build system, Trae support, improved skill descriptions, and security hardening. Consolidates the critique skill to combine v2.0's sub-agent architecture and automated anti-pattern detection with main's Nielsen heuristics scoring, cognitive load assessment, persona-based testing, and structured follow-up workflow. Fixes browser detector build to create target directory after skill sync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,5 +40,6 @@ ${code}
|
||||
})();
|
||||
`;
|
||||
|
||||
fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
|
||||
fs.writeFileSync(OUTPUT, output);
|
||||
console.log(`\u2713 Generated ${path.relative(ROOT, OUTPUT)} (${(output.length / 1024).toFixed(1)} KB)`);
|
||||
|
||||
+30
-57
@@ -18,16 +18,7 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { readSourceFiles, readPatterns } from './lib/utils.js';
|
||||
import {
|
||||
transformCursor,
|
||||
transformClaudeCode,
|
||||
transformGemini,
|
||||
transformCodex,
|
||||
transformAgents,
|
||||
transformKiro,
|
||||
transformOpenCode,
|
||||
transformPi
|
||||
} from './lib/transformers/index.js';
|
||||
import { createTransformer, PROVIDERS } from './lib/transformers/index.js';
|
||||
import { createAllZips } from './lib/zip.js';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
@@ -83,6 +74,7 @@ async function buildStaticSite() {
|
||||
path.join(ROOT_DIR, 'public', 'index.html'),
|
||||
path.join(ROOT_DIR, 'public', 'cheatsheet.html'),
|
||||
path.join(ROOT_DIR, 'public', 'gallery.html'),
|
||||
path.join(ROOT_DIR, 'public', 'privacy.html'),
|
||||
];
|
||||
const outdir = path.join(ROOT_DIR, 'build');
|
||||
|
||||
@@ -142,18 +134,9 @@ function assembleUniversal(distDir, suffix = '') {
|
||||
fs.rmSync(universalDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const providerMappings = [
|
||||
{ provider: 'cursor', configDir: '.cursor' },
|
||||
{ provider: 'claude-code', configDir: '.claude' },
|
||||
{ provider: 'gemini', configDir: '.gemini' },
|
||||
{ provider: 'codex', configDir: '.codex' },
|
||||
{ provider: 'agents', configDir: '.agents' },
|
||||
{ provider: 'kiro', configDir: '.kiro' },
|
||||
{ provider: 'opencode', configDir: '.opencode' },
|
||||
{ provider: 'pi', configDir: '.pi' }
|
||||
];
|
||||
const providerConfigs = Object.values(PROVIDERS);
|
||||
|
||||
for (const { provider, configDir } of providerMappings) {
|
||||
for (const { provider, configDir } of providerConfigs) {
|
||||
const src = path.join(distDir, `${provider}${suffix}`, configDir);
|
||||
const dest = path.join(universalDir, configDir);
|
||||
if (fs.existsSync(src)) {
|
||||
@@ -178,13 +161,15 @@ This folder contains skills for all supported tools:
|
||||
.kiro/ → Kiro
|
||||
.opencode/ → OpenCode
|
||||
.pi/ → Pi
|
||||
.trae-cn/ → Trae China
|
||||
.trae/ → Trae International
|
||||
|
||||
To install, copy the relevant folder(s) into your project root.
|
||||
These are hidden folders (dotfiles) — press Cmd+Shift+. in Finder to see them.
|
||||
`);
|
||||
|
||||
const label = suffix ? ' (prefixed)' : '';
|
||||
console.log(`✓ Assembled universal${label} directory (${providerMappings.length} providers)`);
|
||||
console.log(`✓ Assembled universal${label} directory (${providerConfigs.length} providers)`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,12 +186,12 @@ function generateApiData(buildDir, skills, patterns) {
|
||||
id: path.basename(path.dirname(s.filePath)),
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
userInvokable: s.userInvokable,
|
||||
userInvocable: s.userInvocable,
|
||||
}));
|
||||
fs.writeFileSync(path.join(apiDir, 'skills.json'), JSON.stringify(skillsData));
|
||||
|
||||
// commands.json (user-invokable skills only)
|
||||
const commandsData = skillsData.filter(s => s.userInvokable);
|
||||
// commands.json (user-invocable skills only)
|
||||
const commandsData = skillsData.filter(s => s.userInvocable);
|
||||
fs.writeFileSync(path.join(apiDir, 'commands.json'), JSON.stringify(commandsData));
|
||||
|
||||
// patterns.json
|
||||
@@ -338,29 +323,15 @@ async function build() {
|
||||
// Read source files (unified skills architecture)
|
||||
const { skills } = readSourceFiles(ROOT_DIR);
|
||||
const patterns = readPatterns(ROOT_DIR);
|
||||
const userInvokableCount = skills.filter(s => s.userInvokable).length;
|
||||
console.log(`📖 Read ${skills.length} skills (${userInvokableCount} user-invokable) and ${patterns.patterns.length + patterns.antipatterns.length} pattern categories\n`);
|
||||
const userInvocableCount = skills.filter(s => s.userInvocable).length;
|
||||
console.log(`📖 Read ${skills.length} skills (${userInvocableCount} user-invocable) and ${patterns.patterns.length + patterns.antipatterns.length} pattern categories\n`);
|
||||
|
||||
// Transform for each provider
|
||||
transformCursor(skills, DIST_DIR, patterns);
|
||||
transformClaudeCode(skills, DIST_DIR, patterns);
|
||||
transformGemini(skills, DIST_DIR, patterns);
|
||||
transformCodex(skills, DIST_DIR, patterns);
|
||||
transformAgents(skills, DIST_DIR, patterns);
|
||||
transformKiro(skills, DIST_DIR, patterns);
|
||||
transformOpenCode(skills, DIST_DIR, patterns);
|
||||
transformPi(skills, DIST_DIR, patterns);
|
||||
|
||||
// Transform for each provider (prefixed with i-)
|
||||
const prefixOptions = { prefix: 'i-', outputSuffix: '-prefixed' };
|
||||
transformCursor(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformClaudeCode(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformGemini(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformCodex(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformAgents(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformKiro(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformOpenCode(skills, DIST_DIR, patterns, prefixOptions);
|
||||
transformPi(skills, DIST_DIR, patterns, prefixOptions);
|
||||
// Transform for each provider (unprefixed + prefixed)
|
||||
for (const config of Object.values(PROVIDERS)) {
|
||||
const transform = createTransformer(config);
|
||||
transform(skills, DIST_DIR);
|
||||
transform(skills, DIST_DIR, { prefix: 'i-', outputSuffix: '-prefixed' });
|
||||
}
|
||||
|
||||
// Assemble universal directory (unprefixed and prefixed)
|
||||
assembleUniversal(DIST_DIR);
|
||||
@@ -374,19 +345,21 @@ async function build() {
|
||||
copyDistToBuild(DIST_DIR, buildDir);
|
||||
generateCFConfig(buildDir);
|
||||
|
||||
// Copy Claude Code output to project's .claude directory for local development
|
||||
const claudeCodeSrc = path.join(DIST_DIR, 'claude-code', '.claude');
|
||||
const claudeCodeDest = path.join(ROOT_DIR, '.claude');
|
||||
// Copy all provider outputs to project root for local testing
|
||||
const syncConfigs = Object.values(PROVIDERS);
|
||||
|
||||
// Copy skills directory (preserves other files like settings.local.json)
|
||||
const skillsSrc = path.join(claudeCodeSrc, 'skills');
|
||||
const skillsDest = path.join(claudeCodeDest, 'skills');
|
||||
for (const { provider, configDir } of syncConfigs) {
|
||||
const skillsSrc = path.join(DIST_DIR, provider, configDir, 'skills');
|
||||
const skillsDest = path.join(ROOT_DIR, configDir, 'skills');
|
||||
|
||||
// Remove existing and copy fresh
|
||||
if (fs.existsSync(skillsDest)) fs.rmSync(skillsDest, { recursive: true });
|
||||
copyDirSync(skillsSrc, skillsDest);
|
||||
if (fs.existsSync(skillsSrc)) {
|
||||
if (fs.existsSync(skillsDest)) fs.rmSync(skillsDest, { recursive: true });
|
||||
copyDirSync(skillsSrc, skillsDest);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📋 Synced skills to: ${syncConfigs.map(p => p.configDir).join(', ')}`);
|
||||
|
||||
console.log(`📋 Synced to .claude/: skills`);
|
||||
|
||||
// Generate browser anti-pattern detector (after skill sync so it doesn't get overwritten)
|
||||
try {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { transformProvider } from './shared.js';
|
||||
|
||||
/**
|
||||
* Agents Transformer (VS Code Copilot + Antigravity)
|
||||
* Output: .agents/skills/{name}/SKILL.md
|
||||
*/
|
||||
export function transformAgents(skills, distDir, patterns = null, options = {}) {
|
||||
transformProvider({
|
||||
provider: 'agents',
|
||||
displayName: 'Agents',
|
||||
configDir: '.agents',
|
||||
buildFrontmatter: (skill, skillName) => {
|
||||
const obj = { name: skillName, description: skill.description };
|
||||
if (skill.userInvokable) obj['user-invokable'] = true;
|
||||
if (skill.userInvokable && skill.args && skill.args.length > 0) {
|
||||
const hints = skill.args.map(arg =>
|
||||
arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`
|
||||
);
|
||||
obj['argument-hint'] = hints.join(' ');
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
}, skills, distDir, options);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { transformProvider } from './shared.js';
|
||||
|
||||
/**
|
||||
* Claude Code Transformer
|
||||
* Output: .claude/skills/{name}/SKILL.md
|
||||
*/
|
||||
export function transformClaudeCode(skills, distDir, patterns = null, options = {}) {
|
||||
transformProvider({
|
||||
provider: 'claude-code',
|
||||
displayName: 'Claude Code',
|
||||
configDir: '.claude',
|
||||
buildFrontmatter: (skill, skillName) => {
|
||||
const obj = { name: skillName, description: skill.description };
|
||||
if (skill.userInvokable) obj['user-invokable'] = true;
|
||||
if (skill.args && skill.args.length > 0) obj.args = skill.args;
|
||||
if (skill.license) obj.license = skill.license;
|
||||
if (skill.compatibility) obj.compatibility = skill.compatibility;
|
||||
if (skill.metadata) obj.metadata = skill.metadata;
|
||||
if (skill.allowedTools) obj['allowed-tools'] = skill.allowedTools;
|
||||
return obj;
|
||||
},
|
||||
}, skills, distDir, options);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { transformProvider } from './shared.js';
|
||||
|
||||
/**
|
||||
* Codex Transformer
|
||||
* Output: .codex/skills/{name}/SKILL.md
|
||||
* User-invokable: {{argname}} becomes $ARGNAME, argument-hint in frontmatter
|
||||
*/
|
||||
export function transformCodex(skills, distDir, patterns = null, options = {}) {
|
||||
transformProvider({
|
||||
provider: 'codex',
|
||||
displayName: 'Codex',
|
||||
configDir: '.codex',
|
||||
buildFrontmatter: (skill, skillName) => {
|
||||
const obj = { name: skillName, description: skill.description };
|
||||
if (skill.userInvokable && skill.args && skill.args.length > 0) {
|
||||
const hints = skill.args.map(arg =>
|
||||
arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`
|
||||
);
|
||||
obj['argument-hint'] = hints.join(' ');
|
||||
}
|
||||
if (skill.license) obj.license = skill.license;
|
||||
return obj;
|
||||
},
|
||||
transformBody: (body, skill) => {
|
||||
if (skill.userInvokable) {
|
||||
return body.replace(/\{\{([^}]+)\}\}/g, (_, argName) => `$${argName.toUpperCase()}`);
|
||||
}
|
||||
return body;
|
||||
},
|
||||
}, skills, distDir, options);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { transformProvider } from './shared.js';
|
||||
|
||||
/**
|
||||
* Cursor Transformer
|
||||
* Output: .cursor/skills/{name}/SKILL.md
|
||||
*/
|
||||
export function transformCursor(skills, distDir, patterns = null, options = {}) {
|
||||
transformProvider({
|
||||
provider: 'cursor',
|
||||
displayName: 'Cursor',
|
||||
configDir: '.cursor',
|
||||
buildFrontmatter: (skill, skillName) => {
|
||||
const obj = { name: skillName, description: skill.description };
|
||||
if (skill.license) obj.license = skill.license;
|
||||
return obj;
|
||||
},
|
||||
}, skills, distDir, options);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders, prefixSkillReferences, PROVIDER_PLACEHOLDERS } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Map from frontmatter field name to extraction spec.
|
||||
*
|
||||
* - sourceKey: property name on the skill object
|
||||
* - yamlKey: key name in YAML frontmatter
|
||||
* - condition: if provided, field is only emitted when this returns true
|
||||
* - value: if provided, use this instead of skill[sourceKey]
|
||||
*/
|
||||
const FIELD_SPECS = {
|
||||
'user-invocable': {
|
||||
sourceKey: 'userInvocable',
|
||||
yamlKey: 'user-invocable',
|
||||
condition: (skill) => skill.userInvocable,
|
||||
value: () => true,
|
||||
},
|
||||
'argument-hint': {
|
||||
sourceKey: 'argumentHint',
|
||||
yamlKey: 'argument-hint',
|
||||
condition: (skill) => skill.userInvocable && skill.argumentHint,
|
||||
},
|
||||
license: {
|
||||
sourceKey: 'license',
|
||||
yamlKey: 'license',
|
||||
},
|
||||
compatibility: {
|
||||
sourceKey: 'compatibility',
|
||||
yamlKey: 'compatibility',
|
||||
},
|
||||
metadata: {
|
||||
sourceKey: 'metadata',
|
||||
yamlKey: 'metadata',
|
||||
},
|
||||
'allowed-tools': {
|
||||
sourceKey: 'allowedTools',
|
||||
yamlKey: 'allowed-tools',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a transformer function for a given provider config.
|
||||
*
|
||||
* @param {Object} config - Provider configuration from providers.js
|
||||
* @returns {Function} transform(skills, distDir, options?)
|
||||
*/
|
||||
export function createTransformer(config) {
|
||||
const { provider, configDir, displayName, frontmatterFields = [], bodyTransform, placeholderProvider } = config;
|
||||
const placeholderKey = placeholderProvider || provider;
|
||||
|
||||
const activeFields = frontmatterFields
|
||||
.map((name) => FIELD_SPECS[name])
|
||||
.filter(Boolean);
|
||||
|
||||
return function transform(skills, distDir, options = {}) {
|
||||
const { prefix = '', outputSuffix = '' } = options;
|
||||
const providerDir = path.join(distDir, `${provider}${outputSuffix}`);
|
||||
const skillsDir = path.join(providerDir, `${configDir}/skills`);
|
||||
|
||||
cleanDir(providerDir);
|
||||
ensureDir(skillsDir);
|
||||
|
||||
const allSkillNames = skills.map((s) => s.name);
|
||||
const commandNames = skills
|
||||
.filter((s) => s.userInvocable)
|
||||
.map((s) => `${prefix}${s.name}`);
|
||||
|
||||
let refCount = 0;
|
||||
|
||||
for (const skill of skills) {
|
||||
const skillName = `${prefix}${skill.name}`;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
|
||||
// Build frontmatter
|
||||
const frontmatterObj = {
|
||||
name: skillName,
|
||||
description: skill.description,
|
||||
};
|
||||
|
||||
for (const spec of activeFields) {
|
||||
if (spec.condition && !spec.condition(skill)) continue;
|
||||
const val = spec.value ? spec.value(skill) : skill[spec.sourceKey];
|
||||
if (val) frontmatterObj[spec.yamlKey] = val;
|
||||
}
|
||||
|
||||
const frontmatter = generateYamlFrontmatter(frontmatterObj);
|
||||
|
||||
// Build body
|
||||
const cmdPrefix = (PROVIDER_PLACEHOLDERS[placeholderKey] || {}).command_prefix || '/';
|
||||
let skillBody = replacePlaceholders(skill.body, placeholderKey, commandNames, allSkillNames);
|
||||
if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames, cmdPrefix);
|
||||
if (bodyTransform) skillBody = bodyTransform(skillBody, skill);
|
||||
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
writeFile(path.join(skillDir, 'SKILL.md'), content);
|
||||
|
||||
// Copy reference files
|
||||
if (skill.references && skill.references.length > 0) {
|
||||
const refDir = path.join(skillDir, 'reference');
|
||||
ensureDir(refDir);
|
||||
for (const ref of skill.references) {
|
||||
const refContent = replacePlaceholders(ref.content, placeholderKey, [], allSkillNames);
|
||||
writeFile(path.join(refDir, `${ref.name}.md`), refContent);
|
||||
refCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userInvocableCount = skills.filter((s) => s.userInvocable).length;
|
||||
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
|
||||
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
|
||||
console.log(`✓ ${displayName}${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
|
||||
};
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { transformProvider } from './shared.js';
|
||||
|
||||
/**
|
||||
* Gemini Transformer
|
||||
* Output: .gemini/skills/{name}/SKILL.md
|
||||
* User-invokable: {{arg}} placeholders become {{args}}
|
||||
*/
|
||||
export function transformGemini(skills, distDir, patterns = null, options = {}) {
|
||||
transformProvider({
|
||||
provider: 'gemini',
|
||||
displayName: 'Gemini',
|
||||
configDir: '.gemini',
|
||||
buildFrontmatter: (skill, skillName) => ({
|
||||
name: skillName,
|
||||
description: skill.description,
|
||||
}),
|
||||
transformBody: (body, skill) => {
|
||||
if (skill.userInvokable) {
|
||||
return body.replace(/\{\{[^}]+\}\}/g, '{{args}}');
|
||||
}
|
||||
return body;
|
||||
},
|
||||
}, skills, distDir, options);
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
export { transformCursor } from './cursor.js';
|
||||
export { transformClaudeCode } from './claude-code.js';
|
||||
export { transformGemini } from './gemini.js';
|
||||
export { transformCodex } from './codex.js';
|
||||
export { transformAgents } from './agents.js';
|
||||
export { transformKiro } from './kiro.js';
|
||||
export { transformOpenCode } from "./opencode.js";
|
||||
export { transformPi } from './pi.js';
|
||||
import { createTransformer } from './factory.js';
|
||||
import { PROVIDERS } from './providers.js';
|
||||
|
||||
export const transformCursor = createTransformer(PROVIDERS.cursor);
|
||||
export const transformClaudeCode = createTransformer(PROVIDERS['claude-code']);
|
||||
export const transformGemini = createTransformer(PROVIDERS.gemini);
|
||||
export const transformCodex = createTransformer(PROVIDERS.codex);
|
||||
export const transformAgents = createTransformer(PROVIDERS.agents);
|
||||
export const transformKiro = createTransformer(PROVIDERS.kiro);
|
||||
export const transformOpenCode = createTransformer(PROVIDERS.opencode);
|
||||
export const transformPi = createTransformer(PROVIDERS.pi);
|
||||
|
||||
export { createTransformer, PROVIDERS };
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { transformProvider } from './shared.js';
|
||||
|
||||
/**
|
||||
* Kiro Transformer
|
||||
* Output: .kiro/skills/{name}/SKILL.md
|
||||
*/
|
||||
export function transformKiro(skills, distDir, patterns = null, options = {}) {
|
||||
transformProvider({
|
||||
provider: 'kiro',
|
||||
displayName: 'Kiro',
|
||||
configDir: '.kiro',
|
||||
buildFrontmatter: (skill, skillName) => {
|
||||
const obj = { name: skillName, description: skill.description };
|
||||
if (skill.license) obj.license = skill.license;
|
||||
if (skill.compatibility) obj.compatibility = skill.compatibility;
|
||||
if (skill.metadata) obj.metadata = skill.metadata;
|
||||
return obj;
|
||||
},
|
||||
}, skills, distDir, options);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { transformProvider } from './shared.js';
|
||||
|
||||
/**
|
||||
* OpenCode Transformer
|
||||
* Output: .opencode/skills/{name}/SKILL.md
|
||||
*/
|
||||
export function transformOpenCode(skills, distDir, patterns = null, options = {}) {
|
||||
transformProvider({
|
||||
provider: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
configDir: '.opencode',
|
||||
buildFrontmatter: (skill, skillName) => {
|
||||
const obj = { name: skillName, description: skill.description };
|
||||
if (skill.userInvokable) obj['user-invokable'] = true;
|
||||
if (skill.args && skill.args.length > 0) obj.args = skill.args;
|
||||
if (skill.license) obj.license = skill.license;
|
||||
if (skill.compatibility) obj.compatibility = skill.compatibility;
|
||||
if (skill.metadata) obj.metadata = skill.metadata;
|
||||
if (skill.allowedTools) obj['allowed-tools'] = skill.allowedTools;
|
||||
return obj;
|
||||
},
|
||||
}, skills, distDir, options);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { transformProvider } from './shared.js';
|
||||
|
||||
/**
|
||||
* Pi Transformer
|
||||
* Output: .pi/skills/{name}/SKILL.md
|
||||
*/
|
||||
export function transformPi(skills, distDir, patterns = null, options = {}) {
|
||||
transformProvider({
|
||||
provider: 'pi',
|
||||
displayName: 'Pi',
|
||||
configDir: '.pi',
|
||||
buildFrontmatter: (skill, skillName) => {
|
||||
const obj = { name: skillName, description: skill.description };
|
||||
if (skill.license) obj.license = skill.license;
|
||||
if (skill.compatibility) obj.compatibility = skill.compatibility;
|
||||
if (skill.metadata) obj.metadata = skill.metadata;
|
||||
return obj;
|
||||
},
|
||||
}, skills, distDir, options);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Provider configurations for the transformer factory.
|
||||
*
|
||||
* Each config specifies:
|
||||
* - provider: key into PROVIDER_PLACEHOLDERS (e.g. 'claude-code')
|
||||
* - configDir: dot-directory name (e.g. '.claude')
|
||||
* - displayName: human-readable name for log output (e.g. 'Claude Code')
|
||||
* - frontmatterFields: which optional fields to emit beyond name + description
|
||||
* - bodyTransform: optional function (body, skill) => transformed body
|
||||
*/
|
||||
export const PROVIDERS = {
|
||||
cursor: {
|
||||
provider: 'cursor',
|
||||
configDir: '.cursor',
|
||||
displayName: 'Cursor',
|
||||
frontmatterFields: ['license', 'compatibility', 'metadata'],
|
||||
},
|
||||
'claude-code': {
|
||||
provider: 'claude-code',
|
||||
configDir: '.claude',
|
||||
displayName: 'Claude Code',
|
||||
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
|
||||
},
|
||||
gemini: {
|
||||
provider: 'gemini',
|
||||
configDir: '.gemini',
|
||||
displayName: 'Gemini',
|
||||
frontmatterFields: [],
|
||||
},
|
||||
codex: {
|
||||
provider: 'codex',
|
||||
configDir: '.codex',
|
||||
displayName: 'Codex',
|
||||
frontmatterFields: ['argument-hint', 'license'],
|
||||
},
|
||||
agents: {
|
||||
provider: 'agents',
|
||||
configDir: '.agents',
|
||||
displayName: 'Agents',
|
||||
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'],
|
||||
},
|
||||
kiro: {
|
||||
provider: 'kiro',
|
||||
configDir: '.kiro',
|
||||
displayName: 'Kiro',
|
||||
frontmatterFields: ['license', 'compatibility', 'metadata'],
|
||||
},
|
||||
opencode: {
|
||||
provider: 'opencode',
|
||||
configDir: '.opencode',
|
||||
displayName: 'OpenCode',
|
||||
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata', 'allowed-tools'],
|
||||
},
|
||||
pi: {
|
||||
provider: 'pi',
|
||||
configDir: '.pi',
|
||||
displayName: 'Pi',
|
||||
frontmatterFields: ['license', 'compatibility', 'metadata', 'allowed-tools'],
|
||||
},
|
||||
'trae-cn': {
|
||||
provider: 'trae-cn',
|
||||
configDir: '.trae-cn',
|
||||
displayName: 'Trae China',
|
||||
placeholderProvider: 'trae',
|
||||
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'],
|
||||
},
|
||||
trae: {
|
||||
provider: 'trae',
|
||||
configDir: '.trae',
|
||||
displayName: 'Trae',
|
||||
frontmatterFields: ['user-invocable', 'argument-hint', 'license', 'compatibility', 'metadata'],
|
||||
},
|
||||
};
|
||||
+62
-19
@@ -64,7 +64,11 @@ export function parseFrontmatter(content) {
|
||||
const value = trimmed.slice(colonIndex + 1).trim();
|
||||
|
||||
if (value) {
|
||||
frontmatter[key] = value === 'true' ? true : value === 'false' ? false : value;
|
||||
// Strip YAML quotes
|
||||
const unquoted = (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))
|
||||
? value.slice(1, -1)
|
||||
: value;
|
||||
frontmatter[key] = unquoted === 'true' ? true : unquoted === 'false' ? false : unquoted;
|
||||
currentKey = key;
|
||||
currentArray = null;
|
||||
} else {
|
||||
@@ -107,7 +111,7 @@ export function readFilesRecursive(dir, fileList = []) {
|
||||
/**
|
||||
* Read and parse all source files (unified skills architecture)
|
||||
* All source lives in source/skills/{name}/SKILL.md
|
||||
* Returns { skills } where each skill has userInvokable flag
|
||||
* Returns { skills } where each skill has userInvocable flag
|
||||
*/
|
||||
export function readSourceFiles(rootDir) {
|
||||
const skillsDir = path.join(rootDir, 'source/skills');
|
||||
@@ -166,8 +170,8 @@ export function readSourceFiles(rootDir) {
|
||||
compatibility: frontmatter.compatibility || '',
|
||||
metadata: frontmatter.metadata || null,
|
||||
allowedTools: frontmatter['allowed-tools'] || '',
|
||||
userInvokable: frontmatter['user-invokable'] === true || frontmatter['user-invokable'] === 'true',
|
||||
args: frontmatter.args || [],
|
||||
userInvocable: frontmatter['user-invocable'] === true || frontmatter['user-invocable'] === 'true',
|
||||
argumentHint: frontmatter['argument-hint'] || '',
|
||||
context: frontmatter.context || null,
|
||||
body,
|
||||
filePath: skillMdPath,
|
||||
@@ -287,42 +291,56 @@ 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: '/'
|
||||
},
|
||||
'trae': {
|
||||
model: 'the model',
|
||||
config_file: 'RULES.md',
|
||||
ask_instruction: 'ask the user directly to clarify what you cannot infer.',
|
||||
command_prefix: '/'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -336,8 +354,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;
|
||||
@@ -347,11 +366,18 @@ 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(new RegExp(`the ${escapeRegex(name)} skill`, 'gi'), `the ${prefixed} skill`);
|
||||
result = result.replace(
|
||||
new RegExp(`(the) ${escapeRegex(name)} skill`, 'gi'),
|
||||
(_, article) => `${article} ${prefixed} skill`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -363,18 +389,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,7 +440,8 @@ export function generateYamlFrontmatter(data) {
|
||||
} else if (typeof value === 'boolean') {
|
||||
lines.push(`${key}: ${value}`);
|
||||
} else {
|
||||
lines.push(`${key}: ${value}`);
|
||||
const needsQuoting = typeof value === 'string' && /^[\[{]/.test(value);
|
||||
lines.push(`${key}: ${needsQuoting ? `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"` : value}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user