#!/usr/bin/env node // Build an opencode port of the maister plugin. // // Source: maister/plugins/maister (Claude Code plugin, vendored clone) // Output: .opencode/skill|maister-*/ skills // .opencode/agent/maister-*.md subagents // .opencode/command/maister-*.md slash commands // // Run after every `git -C maister pull`: // node tools/build-maister-opencode.mjs // // The generated tree is disposable — never edit it by hand, edit the upstream // clone and rebuild. Files that are hand-maintained (the plugin, opencode.json) // are not touched by this script. import { readFileSync, writeFileSync, readdirSync, mkdirSync, rmSync, existsSync, statSync, copyFileSync } from "node:fs" import { join, dirname, basename, relative } from "node:path" import { fileURLToPath } from "node:url" const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..") const SRC = join(ROOT, "maister", "plugins", "maister") const OUT = join(ROOT, ".opencode") const PREFIX = "maister-" // --------------------------------------------------------------------------- // upstream version, for the generated headers // --------------------------------------------------------------------------- function upstreamVersion() { try { return JSON.parse(readFileSync(join(ROOT, "maister", ".claude-plugin", "marketplace.json"), "utf8")).version } catch { return "unknown" } } const VERSION = upstreamVersion() // --------------------------------------------------------------------------- // source inventory // --------------------------------------------------------------------------- function walk(dir, out = []) { if (!existsSync(dir)) return out for (const entry of readdirSync(dir)) { const full = join(dir, entry) if (statSync(full).isDirectory()) walk(full, out) else out.push(full) } return out } const SRC_SKILLS = readdirSync(join(SRC, "skills"), { withFileTypes: true }) .filter((e) => e.isDirectory()) .map((e) => e.name) .sort() const SRC_AGENTS = readdirSync(join(SRC, "agents")) .filter((f) => f.endsWith(".md")) .map((f) => basename(f, ".md")) .sort() const SRC_COMMANDS = readdirSync(join(SRC, "commands")) .filter((f) => f.endsWith(".md")) .map((f) => basename(f, ".md")) .sort() // Path of a skill's support file (relative to the skill dir) -> owning skill, // so a reference can be rewritten to a path that resolves from the project root. const SUPPORT_OWNER = new Map() for (const skill of SRC_SKILLS) { for (const file of walk(join(SRC, "skills", skill))) { const rel = relative(join(SRC, "skills", skill), file) if (rel !== "SKILL.md" && /\.(md|html|mjs|json)$/.test(rel)) { if (!SUPPORT_OWNER.has(rel)) SUPPORT_OWNER.set(rel, skill) } } } // --------------------------------------------------------------------------- // text transforms // --------------------------------------------------------------------------- // Ordered: specific phrases first, general fallbacks last. const REWRITES = [ // --- task tracking: opencode has one list-replacing `todowrite` tool ------- [ /Use `TaskCreate` for all phases \(see Phase Configuration\), then set dependencies with `TaskUpdate addBlockedBy`/g, "Write every phase into the todo list with a single `todowrite` call (all `pending`) and encode phase dependencies in the item text (e.g. `Phase 4: Specification (blocked by 2, 3)`) — `todowrite` replaces the whole list on every call and has no dependency field", ], [ /`TaskCreate` for all phases above \(pending\)/g, "a single `todowrite` call listing every phase above as `pending`", ], [ /Use `TaskCreate` for all phases \(pending\), then set sequential dependencies with `TaskUpdate addBlockedBy`/g, "Write every phase into the todo list with a single `todowrite` call (all `pending`), in the order the phases run — `todowrite` replaces the whole list on every call and has no dependency field", ], [ /use `TaskCreate` for all phases \(pending\), then set sequential dependencies with `TaskUpdate addBlockedBy`/g, "write every phase into the todo list with a single `todowrite` call (all `pending`), in the order the phases run — `todowrite` replaces the whole list on every call and has no dependency field", ], [ /Set dependencies: ([^.]+)\. Phase/g, "Sequence them in item order. Phase", ], [ /mark skipped phases as `completed` with `metadata: \{skipped: true\}`/g, 'mark skipped phases `completed` and append "(skipped)" to their text', ], [ /For phases skipped due to scope \(([^)]*)\), mark `completed` with `metadata: \{skipped: true, reason: "([^"]*)"\}`/g, "For phases skipped due to scope ($1), mark them `completed` and append \"(skipped: $2)\" to their text", ], [ /with `metadata: \{"skipped": true\}\`/g, "with `(skipped)` appended to the item text", ], [ /`TaskCreate` for all phases \(pending\), then `TaskUpdate addBlockedBy` for dependencies/g, "a single `todowrite` call listing every phase as `pending`, with dependencies encoded in the item text", ], [ /For each task group, call `TaskCreate`:/g, "For each task group, add an item in one `todowrite` call (it replaces the whole list, so include every existing item):", ], [ /Set dependencies with `TaskUpdate addBlockedBy` mirroring the plan's dependency chain:/g, "Encode the plan's dependency chain in each item's text (e.g. `Group 3: refactor store (blocked by 1, 2)`):", ], [ /Dependencies set via `TaskUpdate addBlockedBy`/g, "Dependencies encoded in the item text passed to `todowrite`", ], [ /Call `TaskList` to find existing task group items from the planner\. If found, use them\. If not, create them with `TaskCreate` for each task group \(when the planner created none\)\./g, "Read the current todo list (opencode keeps it in context and `todowrite` replaces it wholesale). If the planner left no group items, add one per task group in a single `todowrite` call.", ], [ /`TaskUpdate` to `status: "in_progress"` with `owner: "maister:([a-z-]+)"`/g, "`todowrite` with that group `in_progress` (delegated to `$1`)", ], [ /`TaskUpdate` to `status: "completed"` with `metadata: \{[^}]*\}/g, "`todowrite` with that group `completed` (the outcome details go in `implementation/work-log.md`)", ], [ /`TaskUpdate` to `status: "completed"`/g, "`todowrite` with that item `completed`", ], [ /`TaskUpdate` to `status: "completed"`/g, "`todowrite` with that item `completed`", ], [ /`TaskUpdate` with `status: "completed"` and `metadata: \{"skipped": true\}`/g, '`todowrite` with `status: "completed"` and "(skipped)" appended to the item text', ], [ /`TaskUpdate` with `status: "completed"`/g, '`todowrite` with `status: "completed"`', ], [ /`TaskUpdate` to `status: "in_progress"`/g, "`todowrite` with that item `in_progress`", ], [ /`TaskUpdate` to `in_progress`/g, "`todowrite` with that item `in_progress`", ], [ /`TaskUpdate` to `completed`/g, "`todowrite` with that item `completed`", ], [ /`TaskUpdate` addBlockedBy/g, "dependencies encoded in the `todowrite` item text", ], [ /Use `TaskCreate` tool:/g, "Use the `todowrite` tool (one call carrying the full list):", ], [ /`TaskCreate` for each task group with `Dependencies` AND `Files to Modify` declared in `implementation-plan\.md`/g, "one `todowrite` item per task group, with `Dependencies` and `Files to Modify` from `implementation-plan.md` carried in the item text", ], [ /`TaskCreate` per task group with `Dependencies` AND `Files to Modify` declared in `implementation-plan\.md`/g, "one `todowrite` item per task group, with `Dependencies` and `Files to Modify` from `implementation-plan.md` carried in the item text", ], [ /`TaskCreate` per task group/g, "one `todowrite` item per task group", ], [ /`TaskCreate` for all phases \(see Phase Configuration\)/g, "a single `todowrite` call listing every phase (see Phase Configuration)", ], [ /`TaskCreate` for all phases/g, "a single `todowrite` call listing every phase", ], [ /`TaskCreate` initialization\*\*/g, "todo-list initialization**", ], [ /`TaskCreate`\/?`TaskUpdate`/g, "`todowrite`", ], [ /`TaskCreate`\/`TaskUpdate`/g, "`todowrite`", ], [ /`TaskCreate`/g, "`todowrite`", ], [ /`TaskUpdate`/g, "`todowrite`", ], [ /`TaskList`/g, "the todo list", ], [ /`TaskCreate`\/`TaskUpdate` tools/g, "the `todowrite` tool", ], // bare (un-backticked) mentions the specific rules above did not catch [ /`TaskUpdate addBlockedBy`/g, "dependencies encoded in the `todowrite` item text", ], [ /\bTaskCreate\/TaskUpdate\b/g, "`todowrite`", ], [ /\bTaskUpdate addBlockedBy\b/g, "dependencies encoded in the `todowrite` item text", ], [ /\bTaskCreate IDs\b/g, "`todowrite` items", ], [ /\bTaskCreate initialization\b/g, "todo-list initialization", ], [ /\bTaskCreate\b/g, "`todowrite`", ], [ /\bTaskUpdate\b/g, "`todowrite`", ], [ /\bTaskList\b/g, "the todo list", ], // `todowrite` has no metadata bag, no owner and no dependency field: anything // upstream parked there has to move into the item text or the work log. [ /`metadata: \{[^}]*\}`/g, "(recorded in the work log)", ], [ /\bmetadata: \{[^}]*\}\.?/g, "; the details go in the work log.", ], [ /`todowrite` with `addBlockedBy`/g, "the `todowrite` item order", ], [ /with `addBlockedBy` dependencies/g, "in dependency order", ], [ /\baddBlockedBy\b/g, "dependency order", ], [ /`task_ids: \{\}`/g, "an empty `task_ids` map", ], // --- plan mode ----------------------------------------------------------- [ /Call `EnterPlanMode` and let plan mode run exactly as it normally does \(explore the codebase, design the approach, write the plan, then `ExitPlanMode` for approval\)\. Do not redefine its phases\./g, "Plan without editing any file: explore the codebase, design the approach, write the plan out, then request approval with the `question` tool. opencode has no plan-mode tool; read-only behaviour is what plan mode provided, so respect it for the whole planning pass.", ], [ /Do not call `ExitPlanMode` until the plan reflects/g, "Do not ask for approval until the plan reflects", ], [ /\*\*BLOCKING: Do NOT call `ExitPlanMode` until the plan file contains:\*\*/g, "**BLOCKING: do not ask for approval until the plan file contains:**", ], [ /\*\*Use the `EnterPlanMode` tool to present the fix plan for user approval\.\*\*/g, "**Present the fix plan for approval with the `question` tool before touching any file.**", ], [ /### ExitPlanMode Gate: Mandatory Sections/g, "### Approval Gate: Mandatory Sections", ], [ /If any section is missing, add it before calling ExitPlanMode\./g, "If any section is missing, add it before asking for approval.", ], [ /`EnterPlanMode`\/`ExitPlanMode`/g, "plan mode", ], [ /`EnterPlanMode`/g, "plan mode", ], [ /`ExitPlanMode`/g, "the approval request", ], // --- built-in subagents -------------------------------------------------- [ /subagent_type: `general-purpose`/g, "subagent_type: general", ], [ /subagent_type="general-purpose"/g, 'subagent_type="general"', ], [ /subagent_type: general-purpose/g, "subagent_type: general", ], [ /subagent_type="Explore"/g, 'subagent_type="explore"', ], [ /subagent_type: `Explore`/g, "subagent_type: explore", ], [ /subagent_type: Explore/g, "subagent_type: explore", ], // "Explore" is only the built-in subagent where it names one. Everywhere else // it is an ordinary English verb ("Explore the codebase"), which must survive // untouched — so only the subagent-reference forms are rewritten. [/\bExplore subagents?\b/g, "`explore` subagents"], [/\bExplore agents\b/g, "`explore` agents"], [/\bExplore agent\b/g, "`explore` agent"], [/\bExplore\b(?=\s*\()/g, "`explore`"], [/\bthe Explore\b/g, "the `explore` subagent"], [/\braw Explore agents\b/g, "raw `explore` agent findings"], [ /`general-purpose`/g, "`general`", ], [ /\bgeneral-purpose subagents?\b/g, "`general` subagents", ], // --- tools --------------------------------------------------------------- [ /AskUserQuestion/g, "question tool", ], [ /\bquestion tool tool\b/g, "question tool", ], // Claude Code capitalises its tool names; opencode's are lowercase [/\bWebFetch tool\b/g, "webfetch tool"], [/\bWebSearch tool\b/g, "websearch tool"], [/\bRead tool\b/g, "read tool"], [/\bWrite tool\b/g, "write tool"], [/\bEdit tool\b/g, "edit tool"], [/\bBash tool\b/g, "bash tool"], [/\bGlob tool\b/g, "glob tool"], [/\bGrep tool\b/g, "grep tool"], [/\(Bash\)/g, "(bash tool)"], [/\bvia Bash\b/g, "via the bash tool"], [/\bvia Read\b/g, "via the read tool"], [/\bvia Write\b/g, "via the write tool"], [ /Skill\/Task tools/g, "skill/task tools", ], [ /Skill\/Task tool parameters/g, "skill/task tool parameters", ], [ /`Task` tool/g, "`task` tool", ], [ /\bTask tool\b/g, "`task` tool", ], [ /\bTask tool calls?\b/g, "`task` tool calls", ], [ /\bthe Task tool\b/g, "the `task` tool", ], [ /one `Task` tool-use block/g, "one `task` tool-use block", ], [ /`Task\(G2\)`/g, "`task(G2)`", ], [ /`Task\(G3\)`/g, "`task(G3)`", ], [ /`Task\(G4\)`/g, "`task(G4)`", ], [ /Skill tool/g, "skill tool", ], [ /`Skill` tool/g, "`skill` tool", ], [ /via Task tool \(/g, "via `task` tool (", ], [ /via Task tool\b/g, "via the `task` tool", ], [ /Claude Code's `auto` permission mode/g, "opencode's auto-approve permission mode", ], [ /`acceptEdits`/g, "edit-auto-approve", ], [ /`bypassPermissions`/g, "full-access", ], [ /`default`, edit-auto-approve, `auto`, `plan`, full-access/g, "any opencode permission mode", ], [ /`default`, `acceptEdits`, `auto`, `plan`, `bypassPermissions`/g, "any opencode permission mode", ], [ /\(auto \/ edit-auto-approve \/ full-access modes\)/g, "(any permission mode)", ], [ /\bauto \/ edit-auto-approve \/ full-access\b/g, "any permission mode", ], [ /Claude Code lifecycle events/g, "opencode lifecycle events", ], [ /`SessionStart` \(matcher: `compact`\)/g, "post-compaction system reminder", ], [ /`PreToolUse` \(matcher: `Bash`\)/g, "per-agent `permission.bash` rules", ], [ /`PreToolUse` hook/g, "per-agent bash permission rules", ], [ /auto-discovered by Claude Code/g, "auto-discovered by opencode", ], [ /Claude Code's built-in plan mode/g, "opencode's read-only plan pass", ], [ /Claude Code's plan mode/g, "plan mode", ], [ /\bClaude Code\b/g, "opencode", ], [ /CLAUDE\.md/g, "AGENTS.md", ], [ /claude-md-template\.md/g, "agents-md-template.md", ], // The mockup studio server ships with the skill, so its path is a plain // project-relative path — no plugin-root variable to resolve. [ /\$\{CLAUDE_PLUGIN_ROOT\}\/skills\/mockup-studio\//g, ".opencode/skill/maister-mockup-studio/", ], [ /The plugin root is this plugin's own directory — the one holding\s*`\.claude-plugin\/plugin\.json` — and the variable naming it is set in the session\s*environment\. Use it as written; do not work the directory out and substitute a\s*path of your own\./g, "That path is relative to the project root. Use it exactly as written; do not resolve it by hand or substitute a path of your own.", ], [ /The plugin root is this plugin's own directory — the one holding `\.claude-plugin\/plugin\.json` — and the variable naming it is set in the session environment\. Use it as written rather than substituting a path of your own\./g, "That path is relative to the project root. Use it exactly as written rather than substituting a path of your own.", ], // --- playwright MCP tools get the server name as prefix in opencode ------ [/\bbrowser_(navigate|click|type|fill_form|snapshot|evaluate|take_screenshot|console_messages)\b/g, "playwright_browser_$1"], // --- namespaced identifiers: `maister:x` -> `maister-x` ------------------- // opencode has no plugin namespace, so every skill, agent and command lives // in one flat space and needs the flat prefix instead. The lookbehind keeps // `.maisster/`-style project paths out of it. [/(?/references/x.md` (explicit sibling) and // a bare `references/x.md` that names another skill's file. opencode has no // plugin-relative base directory, so every reference becomes a path from the // project root. Same-skill references keep their relative form — opencode // announces the skill's base directory when it loads one. function rewriteSupportPaths(text, currentSkill) { // explicit `..//...` and `[plugin]/skills//...` forms text = text.replace(/(?:\.\.\/|\[plugin\]\/skills\/|\.\/skills\/)([a-z-]+)\//g, (_, s) => SRC_SKILLS.includes(s) ? `.opencode/skill/${PREFIX}${s}/` : `../${s}/`, ) // bare `references/x.md` etc. that belong to a different skill for (const [rel, owner] of SUPPORT_OWNER) { if (owner === currentSkill) continue text = text.replaceAll(new RegExp(`(?/SKILL.md` — the upstream plugin's own layout text = text.replace(/(? SRC_SKILLS.includes(s) ? `.opencode/skill/${PREFIX}${s}/SKILL.md` : `skills/${s}/SKILL.md`, ) return text } function applyRewrites(text, currentSkill) { for (const [pattern, replacement] of REWRITES) { text = text.replace(pattern, replacement) } return rewriteSupportPaths(text, currentSkill) } // --------------------------------------------------------------------------- // frontmatter // --------------------------------------------------------------------------- function splitFrontmatter(raw) { const m = raw.match(/^---\n([\s\S]*?)\n---\n?/) if (!m) return { frontmatter: null, body: raw } return { frontmatter: m[1], body: raw.slice(m[0].length) } } function parseFrontmatter(fm) { const out = {} for (const line of fm.split("\n")) { const m = line.match(/^([A-Za-z_-]+):\s*(.*)$/) if (m) out[m[1]] = m[2] } return out } function yamlValue(value) { if (value === "") return '""' if (/^[A-Za-z0-9_./@ -]+$/.test(value) && !/^(true|false|null|yes|no|on|off)$/i.test(value)) return value return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` } // --------------------------------------------------------------------------- // generated headers // --------------------------------------------------------------------------- const NOTE = `> **opencode port** — generated from the maister Claude Code plugin v${VERSION} by > \`tools/build-maister-opencode.mjs\`. Do not edit this file; edit \`maister/\` and rebuild. > Tool mapping: the \`question\` tool replaces \`AskUserQuestion\`, \`task\` replaces \`Task\`, > \`todowrite\` replaces \`TaskCreate\`/\`TaskUpdate\`/\`TaskList\`, the \`explore\` and \`general\` > subagents replace \`Explore\` and \`general-purpose\`, \`AGENTS.md\` replaces \`CLAUDE.md\`, and > \`playwright_browser_*\` are MCP tools served by the (disabled by default) \`playwright\` server.` const COMMAND_NOTE = `**opencode port** — generated from the maister Claude Code plugin v${VERSION} by \`tools/build-maister-opencode.mjs\`. Claude Code tool names are mapped as: \`question\` replaces \`AskUserQuestion\`, \`task\` replaces \`Task\`, \`todowrite\` replaces \`TaskCreate\`/\`TaskUpdate\`, \`explore\`/\`general\` replace the built-in \`Explore\`/\`general-purpose\` subagents, and \`AGENTS.md\` replaces \`CLAUDE.md\`.` // --------------------------------------------------------------------------- // generate: skills // --------------------------------------------------------------------------- const AGENT_COLOR = { blue: "info", cyan: "info", green: "success", orange: "warning", yellow: "warning", pink: "secondary", purple: "accent", red: "error", } // Agents the upstream PreToolUse hook whitelisted for full bash access. const FULL_BASH_AGENTS = new Set(["test-suite-runner", "e2e-test-verifier", "user-docs-generator", "docs-operator"]) const DESTRUCTIVE_BASH = { "git stash*": "deny", "git reset --hard*": "deny", "git checkout -- *": "deny", "git checkout .*": "deny", "git clean*": "deny", "git push --force*": "deny", "git push -f*": "deny", "rm -rf*": "deny", } function generateSkills() { const dir = join(OUT, "skill") rmSync(dir, { recursive: true, force: true }) let n = 0 for (const skill of SRC_SKILLS) { const srcDir = join(SRC, "skills", skill) const dstDir = join(dir, PREFIX + skill) const srcMd = join(srcDir, "SKILL.md") if (!existsSync(srcMd)) continue const { frontmatter, body } = splitFrontmatter(readFileSync(srcMd, "utf8")) const meta = frontmatter ? parseFrontmatter(frontmatter) : {} let description = meta.description ?? "" if (meta["argument-hint"]) description += ` Usage: ${meta["argument-hint"]}.` if (meta["user-invocable"] === "false") { description += " Internal skill — not intended for direct user invocation; parent skills call it." } description = applyRewrites(description, skill).replace(/\s+/g, " ").trim() // copy support files verbatim, applying the same rewrites to markdown mkdirSync(dstDir, { recursive: true }) for (const file of walk(srcDir)) { if (file === srcMd) continue const rel = relative(srcDir, file) const dst = join(dstDir, rel) mkdirSync(dirname(dst), { recursive: true }) if (file.endsWith(".md")) writeFileSync(dst, applyRewrites(readFileSync(file, "utf8"), skill)) else copyFileSync(file, dst) } const newBody = injectNote(applyRewrites(body, skill), NOTE) writeFileSync(join(dstDir, "SKILL.md"), `---\nname: ${PREFIX}${skill}\ndescription: ${yamlValue(description)}\n---\n\n${newBody}`) n++ } return n } // --------------------------------------------------------------------------- // generate: agents // --------------------------------------------------------------------------- function generateAgents() { const dir = join(OUT, "agent") rmSync(dir, { recursive: true, force: true }) mkdirSync(dir, { recursive: true }) let n = 0 for (const agent of SRC_AGENTS) { const { frontmatter, body } = splitFrontmatter(readFileSync(join(SRC, "agents", agent + ".md"), "utf8")) const meta = frontmatter ? parseFrontmatter(frontmatter) : {} const lines = [ "---", `description: ${yamlValue(applyRewrites(meta.description ?? "", agent).replace(/\s+/g, " ").trim())}`, "mode: subagent", ] if (meta.color && AGENT_COLOR[meta.color]) lines.push(`color: ${AGENT_COLOR[meta.color]}`) // Claude Code whitelisted a few agents for full bash in a PreToolUse hook. // opencode expresses the same policy declaratively per agent. if (FULL_BASH_AGENTS.has(agent)) { lines.push("permission:", " bash: allow") } else { lines.push("permission:", " bash:") for (const [pattern, action] of Object.entries(DESTRUCTIVE_BASH)) lines.push(` "${pattern}": ${action}`) lines.push(' "*": allow') } lines.push("---") let prompt = applyRewrites(body, null) // `skills:` frontmatter is Claude Code only; opencode has no equivalent, so // the preloaded skill becomes an explicit first instruction. const preloaded = (frontmatter ?? "").match(/^skills:\s*\n((?:\s+-\s+.+\n?)+)/m) if (preloaded) { const names = [...preloaded[1].matchAll(/-\s+(.+)/g)].map((m) => m[1].trim()) const rel = names.map((s) => `.opencode/skill/${PREFIX}${s}/SKILL.md`) prompt = `**Preloaded skill**: read ${rel.map((r) => `\`${r}\``).join(" and ")} first and follow it for every operation below.\n\n${prompt}` } writeFileSync(join(dir, PREFIX + agent + ".md"), `${lines.join("\n")}\n\n${prompt.trim()}\n`) n++ } return n } // --------------------------------------------------------------------------- // generate: commands // --------------------------------------------------------------------------- function generateCommands() { const dir = join(OUT, "command") rmSync(dir, { recursive: true, force: true }) mkdirSync(dir, { recursive: true }) let n = 0 for (const command of SRC_COMMANDS) { const { frontmatter, body } = splitFrontmatter(readFileSync(join(SRC, "commands", command + ".md"), "utf8")) const meta = frontmatter ? parseFrontmatter(frontmatter) : {} let template = applyRewrites(body, null) // Claude Code injects the invoked command's name via a tag; // opencode has no such tag, so substitute the name directly. const tag = new RegExp("`?`?", "g") template = template.replace(tag, `\`${PREFIX}${command}\``).replace(/THIS command only/g, `the \`${PREFIX}${command}\` command only`) // `$ARGUMENTS` is opencode's argument placeholder; Claude Code passed the // invocation string implicitly. if (!template.includes("$ARGUMENTS")) { template = `Invocation arguments: \`$ARGUMENTS\`\n\n${template}` } const description = applyRewrites(meta.description ?? "", command).replace(/\s+/g, " ").trim() writeFileSync( join(dir, PREFIX + command + ".md"), `---\ndescription: ${yamlValue(description)}\nagent: build\n---\n\n${COMMAND_NOTE}\n\n${template.trim()}\n`, ) n++ } return n } // --------------------------------------------------------------------------- function injectNote(body, note) { if (!body.startsWith("#")) return `${note}\n\n${body}` const nl = body.indexOf("\n") const title = body.slice(0, nl) return `${title}\n\n${note}\n${body.slice(nl + 1)}` } // --------------------------------------------------------------------------- if (!existsSync(SRC)) { console.error(`error: maister source not found at ${relative(ROOT, SRC)}\n run: git clone https://github.com/SkillPanel/maister.git maister`) process.exit(1) } mkdirSync(OUT, { recursive: true }) const skills = generateSkills() const agents = generateAgents() const commands = generateCommands() console.log(`maister v${VERSION} -> opencode`) console.log(` ${skills} skills -> .opencode/skill/${PREFIX}*/`) console.log(` ${agents} agents -> .opencode/agent/${PREFIX}*.md`) console.log(` ${commands} commands -> .opencode/command/${PREFIX}*.md`) console.log(`\nRestart opencode to load them.`)