mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-11 09:26:53 -04:00
@martmull v2.0 ;) --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
156 lines
4.7 KiB
JavaScript
156 lines
4.7 KiB
JavaScript
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const {
|
|
PLUGIN_ROOT,
|
|
LEGACY_SKILL_NAMES,
|
|
readText,
|
|
listFiles,
|
|
parseSkillFrontmatter,
|
|
parseQuotedYamlField,
|
|
} = require('./lib');
|
|
|
|
const EXPECTED_CANONICAL_SKILLS = [
|
|
'create-app',
|
|
'develop-app',
|
|
'manage-app',
|
|
'publish-app',
|
|
'use-twenty-mcp',
|
|
];
|
|
|
|
const assertSkills = (fail) => {
|
|
const skillsRoot = path.join(PLUGIN_ROOT, 'skills');
|
|
const skillDirectories = fs
|
|
.readdirSync(skillsRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
|
|
for (const skillName of EXPECTED_CANONICAL_SKILLS) {
|
|
if (!skillDirectories.includes(skillName)) {
|
|
fail(`canonical skill is missing: ${skillName}`);
|
|
}
|
|
}
|
|
|
|
for (const skillName of LEGACY_SKILL_NAMES) {
|
|
if (skillDirectories.includes(skillName)) {
|
|
fail(`legacy skill directory must be transferred out of skills/: ${skillName}`);
|
|
}
|
|
}
|
|
|
|
for (const skillName of skillDirectories) {
|
|
const skillPath = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
const agentsPath = path.join(skillsRoot, skillName, 'agents', 'openai.yaml');
|
|
|
|
if (!fs.existsSync(skillPath)) {
|
|
fail(`${skillName} is missing SKILL.md`);
|
|
continue;
|
|
}
|
|
|
|
const frontmatter = parseSkillFrontmatter(skillPath);
|
|
|
|
if (!frontmatter) {
|
|
fail(`${skillName}/SKILL.md is missing YAML frontmatter`);
|
|
} else {
|
|
const frontmatterKeys = Object.keys(frontmatter).sort();
|
|
|
|
if (frontmatter.name !== skillName) {
|
|
fail(`${skillName}/SKILL.md frontmatter name must match its directory`);
|
|
}
|
|
|
|
if (!frontmatter.description) {
|
|
fail(`${skillName}/SKILL.md frontmatter description is required`);
|
|
}
|
|
|
|
if (frontmatterKeys.some((key) => !['description', 'name'].includes(key))) {
|
|
fail(`${skillName}/SKILL.md frontmatter should only include name and description`);
|
|
}
|
|
}
|
|
|
|
if (!fs.existsSync(agentsPath)) {
|
|
fail(`${skillName} is missing agents/openai.yaml`);
|
|
continue;
|
|
}
|
|
|
|
const agentsYaml = readText(agentsPath);
|
|
const displayName = parseQuotedYamlField(agentsYaml, 'display_name');
|
|
const shortDescription = parseQuotedYamlField(agentsYaml, 'short_description');
|
|
const defaultPrompt = parseQuotedYamlField(agentsYaml, 'default_prompt');
|
|
|
|
if (!displayName) {
|
|
fail(`${skillName}/agents/openai.yaml is missing interface.display_name`);
|
|
}
|
|
|
|
if (!shortDescription) {
|
|
fail(`${skillName}/agents/openai.yaml is missing interface.short_description`);
|
|
} else if (shortDescription.length > 64) {
|
|
fail(`${skillName}/agents/openai.yaml short_description must be 64 characters or fewer`);
|
|
}
|
|
|
|
if (!defaultPrompt) {
|
|
fail(`${skillName}/agents/openai.yaml is missing interface.default_prompt`);
|
|
} else if (!defaultPrompt.includes(`$${skillName}`)) {
|
|
fail(`${skillName}/agents/openai.yaml default_prompt must mention $${skillName}`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const assertSkillTriggerPhrases = (fail) => {
|
|
const skillsRoot = path.join(PLUGIN_ROOT, 'skills');
|
|
const skillDirectories = fs
|
|
.readdirSync(skillsRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name);
|
|
|
|
for (const skillName of skillDirectories) {
|
|
const skillPath = path.join(skillsRoot, skillName, 'SKILL.md');
|
|
|
|
if (!fs.existsSync(skillPath)) {
|
|
continue;
|
|
}
|
|
|
|
const contents = readText(skillPath);
|
|
|
|
if (!/^#+\s+When To Use\s*$/m.test(contents)) {
|
|
fail(`${skillName}/SKILL.md must include a "When To Use" section with representative trigger phrases`);
|
|
}
|
|
}
|
|
};
|
|
|
|
const assertNoLegacySkillReferences = (fail) => {
|
|
const filesToCheck = listFiles(PLUGIN_ROOT).filter((filePath) => {
|
|
const extension = path.extname(filePath);
|
|
|
|
return ['.md', '.yaml', '.yml'].includes(extension);
|
|
});
|
|
|
|
for (const filePath of filesToCheck) {
|
|
const relativePath = path.relative(PLUGIN_ROOT, filePath);
|
|
const contents = readText(filePath);
|
|
|
|
for (const legacySkillName of LEGACY_SKILL_NAMES) {
|
|
if (contents.includes(`name: ${legacySkillName}`)) {
|
|
fail(`${relativePath} must not declare legacy skill name ${legacySkillName}`);
|
|
}
|
|
|
|
const mentionPattern =
|
|
legacySkillName === 'setup-mcp'
|
|
? /(^|[^A-Za-z0-9_-])setup-mcp(?!\.sh)(?=$|[^A-Za-z0-9_-])/
|
|
: new RegExp(
|
|
`(^|[^A-Za-z0-9_-])${legacySkillName}(?=$|[^A-Za-z0-9_-])`,
|
|
);
|
|
|
|
if (mentionPattern.test(contents)) {
|
|
fail(`${relativePath} must not mention legacy skill name ${legacySkillName}`);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
EXPECTED_CANONICAL_SKILLS,
|
|
assertSkills,
|
|
assertSkillTriggerPhrases,
|
|
assertNoLegacySkillReferences,
|
|
};
|