File size: 3,486 Bytes
c04bc04 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | /**
* OpenDesign plugin for OpenCode.ai
*
* Injects the opendesign bootstrap skill via chat transform,
* and registers the skills directory via config hook (no symlinks needed).
*/
import path from 'path';
import fs from 'fs';
import os from 'os';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Minimal frontmatter extractor (avoids external deps).
const extractAndStripFrontmatter = (content) => {
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!match) return { frontmatter: {}, content };
const frontmatterStr = match[1];
const body = match[2];
const frontmatter = {};
for (const line of frontmatterStr.split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx > 0) {
const key = line.slice(0, colonIdx).trim();
const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
frontmatter[key] = value;
}
}
return { frontmatter, content: body };
};
const normalizePath = (p, homeDir) => {
if (!p || typeof p !== 'string') return null;
let normalized = p.trim();
if (!normalized) return null;
if (normalized.startsWith('~/')) {
normalized = path.join(homeDir, normalized.slice(2));
} else if (normalized === '~') {
normalized = homeDir;
}
return path.resolve(normalized);
};
export const OpenDesignPlugin = async ({ client, directory }) => {
const homeDir = os.homedir();
const opendesignSkillsDir = path.resolve(__dirname, '../../skills');
const envConfigDir = normalizePath(process.env.OPENCODE_CONFIG_DIR, homeDir);
const configDir = envConfigDir || path.join(homeDir, '.config/opencode');
const getBootstrapContent = () => {
const skillPath = path.join(opendesignSkillsDir, 'opendesign', 'SKILL.md');
if (!fs.existsSync(skillPath)) return null;
const fullContent = fs.readFileSync(skillPath, 'utf8');
const { content } = extractAndStripFrontmatter(fullContent);
const toolMapping = `**Tool Mapping for OpenCode:**
When OpenDesign skills reference tools you don't have, substitute OpenCode equivalents:
- \`TodoWrite\` → \`todowrite\`
- \`Task\` tool with subagents → OpenCode's subagent system (@mention)
- \`Skill\` tool → OpenCode's native \`skill\` tool
- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → your native tools
Use OpenCode's native \`skill\` tool to list and load the other OpenDesign skills (wireframe, make-a-deck, interactive-prototype, etc.) on demand.`;
return `<EXTREMELY_IMPORTANT>
You have OpenDesign loaded.
**The opendesign entry-point skill is included below. It is ALREADY LOADED — you are currently following it. Do NOT use the skill tool to load "opendesign" again.**
${content}
${toolMapping}
</EXTREMELY_IMPORTANT>`;
};
return {
// Register the skills directory so OpenCode discovers every SKILL.md
// without the user editing opencode.json or symlinking.
config: async (config) => {
config.skills = config.skills || {};
config.skills.paths = config.skills.paths || [];
if (!config.skills.paths.includes(opendesignSkillsDir)) {
config.skills.paths.push(opendesignSkillsDir);
}
},
// Use system prompt transform for compatibility with current OpenCode builds.
'experimental.chat.system.transform': async (_input, output) => {
const bootstrap = getBootstrapContent();
if (bootstrap) {
(output.system ||= []).push(bootstrap);
}
}
};
};
|