Files
memind/skills-registry.mjs
T
John 6ee6fd64dd Add MindSpace page live edit, chat skills, and H5 deploy tooling.
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:09:38 -07:00

178 lines
5.2 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
ensurePublishSkillInstalled,
PUBLISH_SKILL_NAME,
renderPublishSkill,
resolvePublicBaseUrl,
} from './user-publish.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const DEFAULT_USER_SKILLS = {
web: true,
search: true,
'form-builder': true,
'table-viewer': true,
[PUBLISH_SKILL_NAME]: false,
};
/** Optional presets for admin role configuration (creator / developer). */
export const USER_ROLE_SKILL_PRESETS = {
user: DEFAULT_USER_SKILLS,
creator: {
...DEFAULT_USER_SKILLS,
[PUBLISH_SKILL_NAME]: true,
kanban: true,
timeline: true,
},
developer: {
...DEFAULT_USER_SKILLS,
git: true,
'diff-viewer': true,
'code-playground': true,
'test-runner': true,
},
};
export function parseSkillFrontmatter(content) {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!match) return { name: null, description: '' };
const block = match[1];
const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? null;
const description = block.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? '';
return { name, description };
}
export function listPlatformSkillCatalog(h5Root = __dirname) {
const skillsRoot = path.join(h5Root, 'skills');
if (!fs.existsSync(skillsRoot)) return [];
const catalog = [];
for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const skillPath = path.join(skillsRoot, entry.name, 'SKILL.md');
if (!fs.existsSync(skillPath)) continue;
const raw = fs.readFileSync(skillPath, 'utf8');
const meta = parseSkillFrontmatter(raw);
const name = meta.name || entry.name;
catalog.push({
name,
dirName: entry.name,
label: name,
description: meta.description || '平台通用技能',
category: 'platform',
requiresPublish: name === PUBLISH_SKILL_NAME,
});
}
return catalog.sort((a, b) => a.name.localeCompare(b.name));
}
export function catalogSkillNames(catalog) {
return catalog.map((item) => item.name);
}
export function isValidSkillName(catalog, name) {
return catalog.some((item) => item.name === name);
}
export function normalizeSkillPatch(catalog, patch) {
const normalized = {};
for (const [key, value] of Object.entries(patch ?? {})) {
if (!isValidSkillName(catalog, key)) continue;
normalized[key] = Boolean(value);
}
return normalized;
}
export function resolveSkillMap(roleGrants, userOverrides, catalog) {
const resolved = {};
for (const item of catalog) {
const key = item.name;
if (key in userOverrides) {
resolved[key] = userOverrides[key];
} else if (key in roleGrants) {
resolved[key] = roleGrants[key];
} else {
resolved[key] = DEFAULT_USER_SKILLS[key] ?? false;
}
}
return resolved;
}
export function grantedSkillNames(skillMap) {
return Object.entries(skillMap)
.filter(([, enabled]) => enabled)
.map(([name]) => name);
}
export function applySkillGrantsToCapabilities(capabilities, skillMap) {
const effective = { ...capabilities };
const enabled = grantedSkillNames(skillMap);
if (enabled.length > 0) {
effective.skills = true;
}
if (enabled.includes(PUBLISH_SKILL_NAME)) {
effective.static_publish = true;
}
return effective;
}
function copySkillTree(srcDir, destDir) {
fs.mkdirSync(destDir, { recursive: true });
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
const from = path.join(srcDir, entry.name);
const to = path.join(destDir, entry.name);
if (entry.isDirectory()) {
copySkillTree(from, to);
} else {
fs.copyFileSync(from, to);
}
}
}
export function syncSkillsToWorkspace({
h5Root,
publishDir,
skillMap,
catalog,
user,
publicBaseUrl,
}) {
const enabled = new Set(grantedSkillNames(skillMap));
const platformNames = new Set(catalog.map((item) => item.name));
const agentsSkills = path.join(publishDir, '.agents', 'skills');
if (fs.existsSync(agentsSkills)) {
for (const entry of fs.readdirSync(agentsSkills, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const skillName = entry.name;
const catalogItem = catalog.find((item) => item.name === skillName || item.dirName === skillName);
const resolvedName = catalogItem?.name ?? skillName;
if (platformNames.has(resolvedName) && !enabled.has(resolvedName)) {
fs.rmSync(path.join(agentsSkills, entry.name), { recursive: true, force: true });
}
}
}
for (const item of catalog) {
if (!enabled.has(item.name)) continue;
const srcDir = path.join(h5Root, 'skills', item.dirName);
const destDir = path.join(agentsSkills, item.name);
if (item.name === PUBLISH_SKILL_NAME && user) {
ensurePublishSkillInstalled(publishDir, {
slug: String(user.id).trim().toLowerCase(),
username: user.username ? String(user.username).trim().toLowerCase() : undefined,
publicBaseUrl,
publishDir,
});
continue;
}
if (fs.existsSync(srcDir)) {
if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true });
copySkillTree(srcDir, destDir);
}
}
}