Bonapara/twenty codex plugin (#20857)
@martmull v2.0 ;) --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
committed by
GitHub
parent
cb744b2eeb
commit
1642be86f5
@@ -0,0 +1,178 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const PLUGIN_ROOT = path.resolve(__dirname, '..', '..');
|
||||
const REPO_ROOT = path.resolve(PLUGIN_ROOT, '..', '..');
|
||||
|
||||
const PUBLIC_DOCS_MCP_SERVER_NAME = 'twenty-docs';
|
||||
const PUBLIC_DOCS_MCP_URL = 'https://docs.twenty.com/mcp';
|
||||
const LEGACY_SKILL_NAMES = [
|
||||
'app-readme-and-visuals',
|
||||
'build-app-features',
|
||||
'create-an-app',
|
||||
'design-front-components',
|
||||
'retrieve-and-present-data',
|
||||
'setup-mcp',
|
||||
];
|
||||
|
||||
const VALID_CAPABILITIES = new Set(['Interactive', 'Read', 'Write']);
|
||||
const VALID_CATEGORIES = new Set(['Coding', 'Productivity', 'Communication', 'Data', 'Design', 'Marketing', 'Sales']);
|
||||
const SHORT_DESCRIPTION_MAX = 64;
|
||||
const MIN_LOGO_DIMENSION = 256;
|
||||
|
||||
const readText = (filePath) => fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const listFiles = (directory) => {
|
||||
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
||||
const files = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...listFiles(absolutePath));
|
||||
} else {
|
||||
files.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
};
|
||||
|
||||
const parseSkillFrontmatter = (skillPath) => {
|
||||
const contents = readText(skillPath);
|
||||
const match = contents.match(/^---\n([\s\S]*?)\n---\n/);
|
||||
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const frontmatter = {};
|
||||
|
||||
for (const line of match[1].split('\n')) {
|
||||
const fieldMatch = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
|
||||
|
||||
if (fieldMatch) {
|
||||
frontmatter[fieldMatch[1]] = fieldMatch[2].replace(/^["']|["']$/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
return frontmatter;
|
||||
};
|
||||
|
||||
const parseQuotedYamlField = (contents, fieldName) => {
|
||||
const match = contents.match(new RegExp(`^\\s+${fieldName}:\\s+"([^"]+)"\\s*$`, 'm'));
|
||||
|
||||
return match?.[1];
|
||||
};
|
||||
|
||||
const isAllowedDocumentationHost = (hostname) => {
|
||||
const host = hostname.toLowerCase();
|
||||
|
||||
return (
|
||||
host === 'localhost' ||
|
||||
host.endsWith('.localhost') ||
|
||||
host.startsWith('127.') ||
|
||||
host === '[::1]' ||
|
||||
host === 'example.com' ||
|
||||
host.endsWith('.example.com') ||
|
||||
host === 'example.twenty.com' ||
|
||||
host === 'myworkspace.twenty.com' ||
|
||||
host === 'myworkspace.customdomain.com' ||
|
||||
host === 'your-twenty-server.com' ||
|
||||
host === 'app.twenty.com' ||
|
||||
host === 'twenty.com' ||
|
||||
host === 'docs.twenty.com' ||
|
||||
host === 'www.docker.com' ||
|
||||
host === 'github.com' ||
|
||||
host === 'www.w3.org' ||
|
||||
host === 'developers.openai.com' ||
|
||||
host === 'keepachangelog.com' ||
|
||||
host === 'semver.org' ||
|
||||
host.endsWith('.example')
|
||||
);
|
||||
};
|
||||
|
||||
const readPngDimensions = (filePath) => {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
|
||||
if (buffer.length < 24 || !buffer.subarray(0, 8).equals(signature)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (buffer.subarray(12, 16).toString('ascii') !== 'IHDR') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
width: buffer.readUInt32BE(16),
|
||||
height: buffer.readUInt32BE(20),
|
||||
};
|
||||
};
|
||||
|
||||
const createJsonReaders = (fail) => {
|
||||
const readJson = (relativePath) => {
|
||||
const absolutePath = path.join(REPO_ROOT, relativePath);
|
||||
|
||||
try {
|
||||
return JSON.parse(readText(absolutePath));
|
||||
} catch (error) {
|
||||
fail(`${relativePath} is not valid JSON: ${error.message}`);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const readOptionalJson = (relativePath) => {
|
||||
const absolutePath = path.join(REPO_ROOT, relativePath);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return readJson(relativePath);
|
||||
};
|
||||
|
||||
return { readJson, readOptionalJson };
|
||||
};
|
||||
|
||||
const createInterfacePathResolver = (fail) => (relativePath) => {
|
||||
if (typeof relativePath !== 'string' || relativePath.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!relativePath.startsWith('./')) {
|
||||
fail(`interface path must start with ./ (got: ${relativePath})`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(PLUGIN_ROOT, relativePath.slice(2));
|
||||
|
||||
// Reject ../ traversal that escapes the plugin directory after normalization
|
||||
if (resolvedPath !== PLUGIN_ROOT && !resolvedPath.startsWith(PLUGIN_ROOT + path.sep)) {
|
||||
fail(`interface path must stay within the plugin directory (got: ${relativePath})`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resolvedPath;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
PLUGIN_ROOT,
|
||||
REPO_ROOT,
|
||||
PUBLIC_DOCS_MCP_SERVER_NAME,
|
||||
PUBLIC_DOCS_MCP_URL,
|
||||
LEGACY_SKILL_NAMES,
|
||||
VALID_CAPABILITIES,
|
||||
VALID_CATEGORIES,
|
||||
SHORT_DESCRIPTION_MAX,
|
||||
MIN_LOGO_DIMENSION,
|
||||
readText,
|
||||
listFiles,
|
||||
parseSkillFrontmatter,
|
||||
parseQuotedYamlField,
|
||||
isAllowedDocumentationHost,
|
||||
readPngDimensions,
|
||||
createJsonReaders,
|
||||
createInterfacePathResolver,
|
||||
};
|
||||
Reference in New Issue
Block a user