build-maister-opencode.mjs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  1. #!/usr/bin/env node
  2. // Build an opencode port of the maister plugin.
  3. //
  4. // Source: maister/plugins/maister (Claude Code plugin, vendored clone)
  5. // Output: .opencode/skill|maister-*/ skills
  6. // .opencode/agent/maister-*.md subagents
  7. // .opencode/command/maister-*.md slash commands
  8. //
  9. // Run after every `git -C maister pull`:
  10. // node tools/build-maister-opencode.mjs
  11. //
  12. // The generated tree is disposable — never edit it by hand, edit the upstream
  13. // clone and rebuild. Files that are hand-maintained (the plugin, opencode.json)
  14. // are not touched by this script.
  15. import { readFileSync, writeFileSync, readdirSync, mkdirSync, rmSync, existsSync, statSync, copyFileSync } from "node:fs"
  16. import { join, dirname, basename, relative } from "node:path"
  17. import { fileURLToPath } from "node:url"
  18. const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..")
  19. const SRC = join(ROOT, "maister", "plugins", "maister")
  20. const OUT = join(ROOT, ".opencode")
  21. const PREFIX = "maister-"
  22. // ---------------------------------------------------------------------------
  23. // upstream version, for the generated headers
  24. // ---------------------------------------------------------------------------
  25. function upstreamVersion() {
  26. try {
  27. return JSON.parse(readFileSync(join(ROOT, "maister", ".claude-plugin", "marketplace.json"), "utf8")).version
  28. } catch {
  29. return "unknown"
  30. }
  31. }
  32. const VERSION = upstreamVersion()
  33. // ---------------------------------------------------------------------------
  34. // source inventory
  35. // ---------------------------------------------------------------------------
  36. function walk(dir, out = []) {
  37. if (!existsSync(dir)) return out
  38. for (const entry of readdirSync(dir)) {
  39. const full = join(dir, entry)
  40. if (statSync(full).isDirectory()) walk(full, out)
  41. else out.push(full)
  42. }
  43. return out
  44. }
  45. const SRC_SKILLS = readdirSync(join(SRC, "skills"), { withFileTypes: true })
  46. .filter((e) => e.isDirectory())
  47. .map((e) => e.name)
  48. .sort()
  49. const SRC_AGENTS = readdirSync(join(SRC, "agents"))
  50. .filter((f) => f.endsWith(".md"))
  51. .map((f) => basename(f, ".md"))
  52. .sort()
  53. const SRC_COMMANDS = readdirSync(join(SRC, "commands"))
  54. .filter((f) => f.endsWith(".md"))
  55. .map((f) => basename(f, ".md"))
  56. .sort()
  57. // Path of a skill's support file (relative to the skill dir) -> owning skill,
  58. // so a reference can be rewritten to a path that resolves from the project root.
  59. const SUPPORT_OWNER = new Map()
  60. for (const skill of SRC_SKILLS) {
  61. for (const file of walk(join(SRC, "skills", skill))) {
  62. const rel = relative(join(SRC, "skills", skill), file)
  63. if (rel !== "SKILL.md" && /\.(md|html|mjs|json)$/.test(rel)) {
  64. if (!SUPPORT_OWNER.has(rel)) SUPPORT_OWNER.set(rel, skill)
  65. }
  66. }
  67. }
  68. // ---------------------------------------------------------------------------
  69. // text transforms
  70. // ---------------------------------------------------------------------------
  71. // Ordered: specific phrases first, general fallbacks last.
  72. const REWRITES = [
  73. // --- task tracking: opencode has one list-replacing `todowrite` tool -------
  74. [
  75. /Use `TaskCreate` for all phases \(see Phase Configuration\), then set dependencies with `TaskUpdate addBlockedBy`/g,
  76. "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",
  77. ],
  78. [
  79. /`TaskCreate` for all phases above \(pending\)/g,
  80. "a single `todowrite` call listing every phase above as `pending`",
  81. ],
  82. [
  83. /Use `TaskCreate` for all phases \(pending\), then set sequential dependencies with `TaskUpdate addBlockedBy`/g,
  84. "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",
  85. ],
  86. [
  87. /use `TaskCreate` for all phases \(pending\), then set sequential dependencies with `TaskUpdate addBlockedBy`/g,
  88. "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",
  89. ],
  90. [
  91. /Set dependencies: ([^.]+)\. Phase/g,
  92. "Sequence them in item order. Phase",
  93. ],
  94. [
  95. /mark skipped phases as `completed` with `metadata: \{skipped: true\}`/g,
  96. 'mark skipped phases `completed` and append "(skipped)" to their text',
  97. ],
  98. [
  99. /For phases skipped due to scope \(([^)]*)\), mark `completed` with `metadata: \{skipped: true, reason: "([^"]*)"\}`/g,
  100. "For phases skipped due to scope ($1), mark them `completed` and append \"(skipped: $2)\" to their text",
  101. ],
  102. [
  103. /with `metadata: \{"skipped": true\}\`/g,
  104. "with `(skipped)` appended to the item text",
  105. ],
  106. [
  107. /`TaskCreate` for all phases \(pending\), then `TaskUpdate addBlockedBy` for dependencies/g,
  108. "a single `todowrite` call listing every phase as `pending`, with dependencies encoded in the item text",
  109. ],
  110. [
  111. /For each task group, call `TaskCreate`:/g,
  112. "For each task group, add an item in one `todowrite` call (it replaces the whole list, so include every existing item):",
  113. ],
  114. [
  115. /Set dependencies with `TaskUpdate addBlockedBy` mirroring the plan's dependency chain:/g,
  116. "Encode the plan's dependency chain in each item's text (e.g. `Group 3: refactor store (blocked by 1, 2)`):",
  117. ],
  118. [
  119. /Dependencies set via `TaskUpdate addBlockedBy`/g,
  120. "Dependencies encoded in the item text passed to `todowrite`",
  121. ],
  122. [
  123. /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,
  124. "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.",
  125. ],
  126. [
  127. /`TaskUpdate` to `status: "in_progress"` with `owner: "maister:([a-z-]+)"`/g,
  128. "`todowrite` with that group `in_progress` (delegated to `$1`)",
  129. ],
  130. [
  131. /`TaskUpdate` to `status: "completed"` with `metadata: \{[^}]*\}/g,
  132. "`todowrite` with that group `completed` (the outcome details go in `implementation/work-log.md`)",
  133. ],
  134. [
  135. /`TaskUpdate` to `status: "completed"`/g,
  136. "`todowrite` with that item `completed`",
  137. ],
  138. [
  139. /`TaskUpdate` to `status: "completed"`/g,
  140. "`todowrite` with that item `completed`",
  141. ],
  142. [
  143. /`TaskUpdate` with `status: "completed"` and `metadata: \{"skipped": true\}`/g,
  144. '`todowrite` with `status: "completed"` and "(skipped)" appended to the item text',
  145. ],
  146. [
  147. /`TaskUpdate` with `status: "completed"`/g,
  148. '`todowrite` with `status: "completed"`',
  149. ],
  150. [
  151. /`TaskUpdate` to `status: "in_progress"`/g,
  152. "`todowrite` with that item `in_progress`",
  153. ],
  154. [
  155. /`TaskUpdate` to `in_progress`/g,
  156. "`todowrite` with that item `in_progress`",
  157. ],
  158. [
  159. /`TaskUpdate` to `completed`/g,
  160. "`todowrite` with that item `completed`",
  161. ],
  162. [
  163. /`TaskUpdate` addBlockedBy/g,
  164. "dependencies encoded in the `todowrite` item text",
  165. ],
  166. [
  167. /Use `TaskCreate` tool:/g,
  168. "Use the `todowrite` tool (one call carrying the full list):",
  169. ],
  170. [
  171. /`TaskCreate` for each task group with `Dependencies` AND `Files to Modify` declared in `implementation-plan\.md`/g,
  172. "one `todowrite` item per task group, with `Dependencies` and `Files to Modify` from `implementation-plan.md` carried in the item text",
  173. ],
  174. [
  175. /`TaskCreate` per task group with `Dependencies` AND `Files to Modify` declared in `implementation-plan\.md`/g,
  176. "one `todowrite` item per task group, with `Dependencies` and `Files to Modify` from `implementation-plan.md` carried in the item text",
  177. ],
  178. [
  179. /`TaskCreate` per task group/g,
  180. "one `todowrite` item per task group",
  181. ],
  182. [
  183. /`TaskCreate` for all phases \(see Phase Configuration\)/g,
  184. "a single `todowrite` call listing every phase (see Phase Configuration)",
  185. ],
  186. [
  187. /`TaskCreate` for all phases/g,
  188. "a single `todowrite` call listing every phase",
  189. ],
  190. [
  191. /`TaskCreate` initialization\*\*/g,
  192. "todo-list initialization**",
  193. ],
  194. [
  195. /`TaskCreate`\/?`TaskUpdate`/g,
  196. "`todowrite`",
  197. ],
  198. [
  199. /`TaskCreate`\/`TaskUpdate`/g,
  200. "`todowrite`",
  201. ],
  202. [
  203. /`TaskCreate`/g,
  204. "`todowrite`",
  205. ],
  206. [
  207. /`TaskUpdate`/g,
  208. "`todowrite`",
  209. ],
  210. [
  211. /`TaskList`/g,
  212. "the todo list",
  213. ],
  214. [
  215. /`TaskCreate`\/`TaskUpdate` tools/g,
  216. "the `todowrite` tool",
  217. ],
  218. // bare (un-backticked) mentions the specific rules above did not catch
  219. [
  220. /`TaskUpdate addBlockedBy`/g,
  221. "dependencies encoded in the `todowrite` item text",
  222. ],
  223. [
  224. /\bTaskCreate\/TaskUpdate\b/g,
  225. "`todowrite`",
  226. ],
  227. [
  228. /\bTaskUpdate addBlockedBy\b/g,
  229. "dependencies encoded in the `todowrite` item text",
  230. ],
  231. [
  232. /\bTaskCreate IDs\b/g,
  233. "`todowrite` items",
  234. ],
  235. [
  236. /\bTaskCreate initialization\b/g,
  237. "todo-list initialization",
  238. ],
  239. [
  240. /\bTaskCreate\b/g,
  241. "`todowrite`",
  242. ],
  243. [
  244. /\bTaskUpdate\b/g,
  245. "`todowrite`",
  246. ],
  247. [
  248. /\bTaskList\b/g,
  249. "the todo list",
  250. ],
  251. // `todowrite` has no metadata bag, no owner and no dependency field: anything
  252. // upstream parked there has to move into the item text or the work log.
  253. [
  254. /`metadata: \{[^}]*\}`/g,
  255. "(recorded in the work log)",
  256. ],
  257. [
  258. /\bmetadata: \{[^}]*\}\.?/g,
  259. "; the details go in the work log.",
  260. ],
  261. [
  262. /`todowrite` with `addBlockedBy`/g,
  263. "the `todowrite` item order",
  264. ],
  265. [
  266. /with `addBlockedBy` dependencies/g,
  267. "in dependency order",
  268. ],
  269. [
  270. /\baddBlockedBy\b/g,
  271. "dependency order",
  272. ],
  273. [
  274. /`task_ids: \{\}`/g,
  275. "an empty `task_ids` map",
  276. ],
  277. // --- plan mode -----------------------------------------------------------
  278. [
  279. /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,
  280. "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.",
  281. ],
  282. [
  283. /Do not call `ExitPlanMode` until the plan reflects/g,
  284. "Do not ask for approval until the plan reflects",
  285. ],
  286. [
  287. /\*\*BLOCKING: Do NOT call `ExitPlanMode` until the plan file contains:\*\*/g,
  288. "**BLOCKING: do not ask for approval until the plan file contains:**",
  289. ],
  290. [
  291. /\*\*Use the `EnterPlanMode` tool to present the fix plan for user approval\.\*\*/g,
  292. "**Present the fix plan for approval with the `question` tool before touching any file.**",
  293. ],
  294. [
  295. /### ExitPlanMode Gate: Mandatory Sections/g,
  296. "### Approval Gate: Mandatory Sections",
  297. ],
  298. [
  299. /If any section is missing, add it before calling ExitPlanMode\./g,
  300. "If any section is missing, add it before asking for approval.",
  301. ],
  302. [
  303. /`EnterPlanMode`\/`ExitPlanMode`/g,
  304. "plan mode",
  305. ],
  306. [
  307. /`EnterPlanMode`/g,
  308. "plan mode",
  309. ],
  310. [
  311. /`ExitPlanMode`/g,
  312. "the approval request",
  313. ],
  314. // --- built-in subagents --------------------------------------------------
  315. [
  316. /subagent_type: `general-purpose`/g,
  317. "subagent_type: general",
  318. ],
  319. [
  320. /subagent_type="general-purpose"/g,
  321. 'subagent_type="general"',
  322. ],
  323. [
  324. /subagent_type: general-purpose/g,
  325. "subagent_type: general",
  326. ],
  327. [
  328. /subagent_type="Explore"/g,
  329. 'subagent_type="explore"',
  330. ],
  331. [
  332. /subagent_type: `Explore`/g,
  333. "subagent_type: explore",
  334. ],
  335. [
  336. /subagent_type: Explore/g,
  337. "subagent_type: explore",
  338. ],
  339. // "Explore" is only the built-in subagent where it names one. Everywhere else
  340. // it is an ordinary English verb ("Explore the codebase"), which must survive
  341. // untouched — so only the subagent-reference forms are rewritten.
  342. [/\bExplore subagents?\b/g, "`explore` subagents"],
  343. [/\bExplore agents\b/g, "`explore` agents"],
  344. [/\bExplore agent\b/g, "`explore` agent"],
  345. [/\bExplore\b(?=\s*\()/g, "`explore`"],
  346. [/\bthe Explore\b/g, "the `explore` subagent"],
  347. [/\braw Explore agents\b/g, "raw `explore` agent findings"],
  348. [
  349. /`general-purpose`/g,
  350. "`general`",
  351. ],
  352. [
  353. /\bgeneral-purpose subagents?\b/g,
  354. "`general` subagents",
  355. ],
  356. // --- tools ---------------------------------------------------------------
  357. [
  358. /AskUserQuestion/g,
  359. "question tool",
  360. ],
  361. [
  362. /\bquestion tool tool\b/g,
  363. "question tool",
  364. ],
  365. // Claude Code capitalises its tool names; opencode's are lowercase
  366. [/\bWebFetch tool\b/g, "webfetch tool"],
  367. [/\bWebSearch tool\b/g, "websearch tool"],
  368. [/\bRead tool\b/g, "read tool"],
  369. [/\bWrite tool\b/g, "write tool"],
  370. [/\bEdit tool\b/g, "edit tool"],
  371. [/\bBash tool\b/g, "bash tool"],
  372. [/\bGlob tool\b/g, "glob tool"],
  373. [/\bGrep tool\b/g, "grep tool"],
  374. [/\(Bash\)/g, "(bash tool)"],
  375. [/\bvia Bash\b/g, "via the bash tool"],
  376. [/\bvia Read\b/g, "via the read tool"],
  377. [/\bvia Write\b/g, "via the write tool"],
  378. [
  379. /Skill\/Task tools/g,
  380. "skill/task tools",
  381. ],
  382. [
  383. /Skill\/Task tool parameters/g,
  384. "skill/task tool parameters",
  385. ],
  386. [
  387. /`Task` tool/g,
  388. "`task` tool",
  389. ],
  390. [
  391. /\bTask tool\b/g,
  392. "`task` tool",
  393. ],
  394. [
  395. /\bTask tool calls?\b/g,
  396. "`task` tool calls",
  397. ],
  398. [
  399. /\bthe Task tool\b/g,
  400. "the `task` tool",
  401. ],
  402. [
  403. /one `Task` tool-use block/g,
  404. "one `task` tool-use block",
  405. ],
  406. [
  407. /`Task\(G2\)`/g,
  408. "`task(G2)`",
  409. ],
  410. [
  411. /`Task\(G3\)`/g,
  412. "`task(G3)`",
  413. ],
  414. [
  415. /`Task\(G4\)`/g,
  416. "`task(G4)`",
  417. ],
  418. [
  419. /Skill tool/g,
  420. "skill tool",
  421. ],
  422. [
  423. /`Skill` tool/g,
  424. "`skill` tool",
  425. ],
  426. [
  427. /via Task tool \(/g,
  428. "via `task` tool (",
  429. ],
  430. [
  431. /via Task tool\b/g,
  432. "via the `task` tool",
  433. ],
  434. [
  435. /Claude Code's `auto` permission mode/g,
  436. "opencode's auto-approve permission mode",
  437. ],
  438. [
  439. /`acceptEdits`/g,
  440. "edit-auto-approve",
  441. ],
  442. [
  443. /`bypassPermissions`/g,
  444. "full-access",
  445. ],
  446. [
  447. /`default`, edit-auto-approve, `auto`, `plan`, full-access/g,
  448. "any opencode permission mode",
  449. ],
  450. [
  451. /`default`, `acceptEdits`, `auto`, `plan`, `bypassPermissions`/g,
  452. "any opencode permission mode",
  453. ],
  454. [
  455. /\(auto \/ edit-auto-approve \/ full-access modes\)/g,
  456. "(any permission mode)",
  457. ],
  458. [
  459. /\bauto \/ edit-auto-approve \/ full-access\b/g,
  460. "any permission mode",
  461. ],
  462. [
  463. /Claude Code lifecycle events/g,
  464. "opencode lifecycle events",
  465. ],
  466. [
  467. /`SessionStart` \(matcher: `compact`\)/g,
  468. "post-compaction system reminder",
  469. ],
  470. [
  471. /`PreToolUse` \(matcher: `Bash`\)/g,
  472. "per-agent `permission.bash` rules",
  473. ],
  474. [
  475. /`PreToolUse` hook/g,
  476. "per-agent bash permission rules",
  477. ],
  478. [
  479. /auto-discovered by Claude Code/g,
  480. "auto-discovered by opencode",
  481. ],
  482. [
  483. /Claude Code's built-in plan mode/g,
  484. "opencode's read-only plan pass",
  485. ],
  486. [
  487. /Claude Code's plan mode/g,
  488. "plan mode",
  489. ],
  490. [
  491. /\bClaude Code\b/g,
  492. "opencode",
  493. ],
  494. [
  495. /CLAUDE\.md/g,
  496. "AGENTS.md",
  497. ],
  498. [
  499. /claude-md-template\.md/g,
  500. "agents-md-template.md",
  501. ],
  502. // The mockup studio server ships with the skill, so its path is a plain
  503. // project-relative path — no plugin-root variable to resolve.
  504. [
  505. /\$\{CLAUDE_PLUGIN_ROOT\}\/skills\/mockup-studio\//g,
  506. ".opencode/skill/maister-mockup-studio/",
  507. ],
  508. [
  509. /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,
  510. "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.",
  511. ],
  512. [
  513. /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,
  514. "That path is relative to the project root. Use it exactly as written rather than substituting a path of your own.",
  515. ],
  516. // --- playwright MCP tools get the server name as prefix in opencode ------
  517. [/\bbrowser_(navigate|click|type|fill_form|snapshot|evaluate|take_screenshot|console_messages)\b/g, "playwright_browser_$1"],
  518. // --- namespaced identifiers: `maister:x` -> `maister-x` -------------------
  519. // opencode has no plugin namespace, so every skill, agent and command lives
  520. // in one flat space and needs the flat prefix instead. The lookbehind keeps
  521. // `.maisster/`-style project paths out of it.
  522. [/(?<![\w.-])maister:([a-z][a-z-]*)/g, `${PREFIX}$1`],
  523. // the `[orchestrator-name]` placeholder is a template, not a real identifier
  524. [/(?<![\w.-])maister:\[/g, `${PREFIX}`],
  525. // a wildcard reference to the whole namespace
  526. [/`\/maister:\*`/g, "`/maister-*`"],
  527. ]
  528. // Support-file paths, resolved after the generic rewrites.
  529. //
  530. // Two forms appear upstream: `../<skill>/references/x.md` (explicit sibling) and
  531. // a bare `references/x.md` that names another skill's file. opencode has no
  532. // plugin-relative base directory, so every reference becomes a path from the
  533. // project root. Same-skill references keep their relative form — opencode
  534. // announces the skill's base directory when it loads one.
  535. function rewriteSupportPaths(text, currentSkill) {
  536. // explicit `../<skill>/...` and `[plugin]/skills/<skill>/...` forms
  537. text = text.replace(/(?:\.\.\/|\[plugin\]\/skills\/|\.\/skills\/)([a-z-]+)\//g, (_, s) =>
  538. SRC_SKILLS.includes(s) ? `.opencode/skill/${PREFIX}${s}/` : `../${s}/`,
  539. )
  540. // bare `references/x.md` etc. that belong to a different skill
  541. for (const [rel, owner] of SUPPORT_OWNER) {
  542. if (owner === currentSkill) continue
  543. text = text.replaceAll(new RegExp(`(?<![\\w/.-])${rel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g"), `.opencode/skill/${PREFIX}${owner}/${rel}`)
  544. }
  545. // `skills/<skill>/SKILL.md` — the upstream plugin's own layout
  546. text = text.replace(/(?<![\w./-])skills\/([a-z-]+)\/[Ss][Kk][Ii][Ll][Ll]\.md/g, (_, s) =>
  547. SRC_SKILLS.includes(s) ? `.opencode/skill/${PREFIX}${s}/SKILL.md` : `skills/${s}/SKILL.md`,
  548. )
  549. return text
  550. }
  551. function applyRewrites(text, currentSkill) {
  552. for (const [pattern, replacement] of REWRITES) {
  553. text = text.replace(pattern, replacement)
  554. }
  555. return rewriteSupportPaths(text, currentSkill)
  556. }
  557. // ---------------------------------------------------------------------------
  558. // frontmatter
  559. // ---------------------------------------------------------------------------
  560. function splitFrontmatter(raw) {
  561. const m = raw.match(/^---\n([\s\S]*?)\n---\n?/)
  562. if (!m) return { frontmatter: null, body: raw }
  563. return { frontmatter: m[1], body: raw.slice(m[0].length) }
  564. }
  565. function parseFrontmatter(fm) {
  566. const out = {}
  567. for (const line of fm.split("\n")) {
  568. const m = line.match(/^([A-Za-z_-]+):\s*(.*)$/)
  569. if (m) out[m[1]] = m[2]
  570. }
  571. return out
  572. }
  573. function yamlValue(value) {
  574. if (value === "") return '""'
  575. if (/^[A-Za-z0-9_./@ -]+$/.test(value) && !/^(true|false|null|yes|no|on|off)$/i.test(value)) return value
  576. return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
  577. }
  578. // ---------------------------------------------------------------------------
  579. // generated headers
  580. // ---------------------------------------------------------------------------
  581. const NOTE = `> **opencode port** — generated from the maister Claude Code plugin v${VERSION} by
  582. > \`tools/build-maister-opencode.mjs\`. Do not edit this file; edit \`maister/\` and rebuild.
  583. > Tool mapping: the \`question\` tool replaces \`AskUserQuestion\`, \`task\` replaces \`Task\`,
  584. > \`todowrite\` replaces \`TaskCreate\`/\`TaskUpdate\`/\`TaskList\`, the \`explore\` and \`general\`
  585. > subagents replace \`Explore\` and \`general-purpose\`, \`AGENTS.md\` replaces \`CLAUDE.md\`, and
  586. > \`playwright_browser_*\` are MCP tools served by the (disabled by default) \`playwright\` server.`
  587. 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\`.`
  588. // ---------------------------------------------------------------------------
  589. // generate: skills
  590. // ---------------------------------------------------------------------------
  591. const AGENT_COLOR = {
  592. blue: "info",
  593. cyan: "info",
  594. green: "success",
  595. orange: "warning",
  596. yellow: "warning",
  597. pink: "secondary",
  598. purple: "accent",
  599. red: "error",
  600. }
  601. // Agents the upstream PreToolUse hook whitelisted for full bash access.
  602. const FULL_BASH_AGENTS = new Set(["test-suite-runner", "e2e-test-verifier", "user-docs-generator", "docs-operator"])
  603. const DESTRUCTIVE_BASH = {
  604. "git stash*": "deny",
  605. "git reset --hard*": "deny",
  606. "git checkout -- *": "deny",
  607. "git checkout .*": "deny",
  608. "git clean*": "deny",
  609. "git push --force*": "deny",
  610. "git push -f*": "deny",
  611. "rm -rf*": "deny",
  612. }
  613. function generateSkills() {
  614. const dir = join(OUT, "skill")
  615. rmSync(dir, { recursive: true, force: true })
  616. let n = 0
  617. for (const skill of SRC_SKILLS) {
  618. const srcDir = join(SRC, "skills", skill)
  619. const dstDir = join(dir, PREFIX + skill)
  620. const srcMd = join(srcDir, "SKILL.md")
  621. if (!existsSync(srcMd)) continue
  622. const { frontmatter, body } = splitFrontmatter(readFileSync(srcMd, "utf8"))
  623. const meta = frontmatter ? parseFrontmatter(frontmatter) : {}
  624. let description = meta.description ?? ""
  625. if (meta["argument-hint"]) description += ` Usage: ${meta["argument-hint"]}.`
  626. if (meta["user-invocable"] === "false") {
  627. description += " Internal skill — not intended for direct user invocation; parent skills call it."
  628. }
  629. description = applyRewrites(description, skill).replace(/\s+/g, " ").trim()
  630. // copy support files verbatim, applying the same rewrites to markdown
  631. mkdirSync(dstDir, { recursive: true })
  632. for (const file of walk(srcDir)) {
  633. if (file === srcMd) continue
  634. const rel = relative(srcDir, file)
  635. const dst = join(dstDir, rel)
  636. mkdirSync(dirname(dst), { recursive: true })
  637. if (file.endsWith(".md")) writeFileSync(dst, applyRewrites(readFileSync(file, "utf8"), skill))
  638. else copyFileSync(file, dst)
  639. }
  640. const newBody = injectNote(applyRewrites(body, skill), NOTE)
  641. writeFileSync(join(dstDir, "SKILL.md"), `---\nname: ${PREFIX}${skill}\ndescription: ${yamlValue(description)}\n---\n\n${newBody}`)
  642. n++
  643. }
  644. return n
  645. }
  646. // ---------------------------------------------------------------------------
  647. // generate: agents
  648. // ---------------------------------------------------------------------------
  649. function generateAgents() {
  650. const dir = join(OUT, "agent")
  651. rmSync(dir, { recursive: true, force: true })
  652. mkdirSync(dir, { recursive: true })
  653. let n = 0
  654. for (const agent of SRC_AGENTS) {
  655. const { frontmatter, body } = splitFrontmatter(readFileSync(join(SRC, "agents", agent + ".md"), "utf8"))
  656. const meta = frontmatter ? parseFrontmatter(frontmatter) : {}
  657. const lines = [
  658. "---",
  659. `description: ${yamlValue(applyRewrites(meta.description ?? "", agent).replace(/\s+/g, " ").trim())}`,
  660. "mode: subagent",
  661. ]
  662. if (meta.color && AGENT_COLOR[meta.color]) lines.push(`color: ${AGENT_COLOR[meta.color]}`)
  663. // Claude Code whitelisted a few agents for full bash in a PreToolUse hook.
  664. // opencode expresses the same policy declaratively per agent.
  665. if (FULL_BASH_AGENTS.has(agent)) {
  666. lines.push("permission:", " bash: allow")
  667. } else {
  668. lines.push("permission:", " bash:")
  669. for (const [pattern, action] of Object.entries(DESTRUCTIVE_BASH)) lines.push(` "${pattern}": ${action}`)
  670. lines.push(' "*": allow')
  671. }
  672. lines.push("---")
  673. let prompt = applyRewrites(body, null)
  674. // `skills:` frontmatter is Claude Code only; opencode has no equivalent, so
  675. // the preloaded skill becomes an explicit first instruction.
  676. const preloaded = (frontmatter ?? "").match(/^skills:\s*\n((?:\s+-\s+.+\n?)+)/m)
  677. if (preloaded) {
  678. const names = [...preloaded[1].matchAll(/-\s+(.+)/g)].map((m) => m[1].trim())
  679. const rel = names.map((s) => `.opencode/skill/${PREFIX}${s}/SKILL.md`)
  680. prompt = `**Preloaded skill**: read ${rel.map((r) => `\`${r}\``).join(" and ")} first and follow it for every operation below.\n\n${prompt}`
  681. }
  682. writeFileSync(join(dir, PREFIX + agent + ".md"), `${lines.join("\n")}\n\n${prompt.trim()}\n`)
  683. n++
  684. }
  685. return n
  686. }
  687. // ---------------------------------------------------------------------------
  688. // generate: commands
  689. // ---------------------------------------------------------------------------
  690. function generateCommands() {
  691. const dir = join(OUT, "command")
  692. rmSync(dir, { recursive: true, force: true })
  693. mkdirSync(dir, { recursive: true })
  694. let n = 0
  695. for (const command of SRC_COMMANDS) {
  696. const { frontmatter, body } = splitFrontmatter(readFileSync(join(SRC, "commands", command + ".md"), "utf8"))
  697. const meta = frontmatter ? parseFrontmatter(frontmatter) : {}
  698. let template = applyRewrites(body, null)
  699. // Claude Code injects the invoked command's name via a <command-name> tag;
  700. // opencode has no such tag, so substitute the name directly.
  701. const tag = new RegExp("`?<command-name>`?", "g")
  702. template = template.replace(tag, `\`${PREFIX}${command}\``).replace(/THIS command only/g, `the \`${PREFIX}${command}\` command only`)
  703. // `$ARGUMENTS` is opencode's argument placeholder; Claude Code passed the
  704. // invocation string implicitly.
  705. if (!template.includes("$ARGUMENTS")) {
  706. template = `Invocation arguments: \`$ARGUMENTS\`\n\n${template}`
  707. }
  708. const description = applyRewrites(meta.description ?? "", command).replace(/\s+/g, " ").trim()
  709. writeFileSync(
  710. join(dir, PREFIX + command + ".md"),
  711. `---\ndescription: ${yamlValue(description)}\nagent: build\n---\n\n${COMMAND_NOTE}\n\n${template.trim()}\n`,
  712. )
  713. n++
  714. }
  715. return n
  716. }
  717. // ---------------------------------------------------------------------------
  718. function injectNote(body, note) {
  719. if (!body.startsWith("#")) return `${note}\n\n${body}`
  720. const nl = body.indexOf("\n")
  721. const title = body.slice(0, nl)
  722. return `${title}\n\n${note}\n${body.slice(nl + 1)}`
  723. }
  724. // ---------------------------------------------------------------------------
  725. if (!existsSync(SRC)) {
  726. console.error(`error: maister source not found at ${relative(ROOT, SRC)}\n run: git clone https://github.com/SkillPanel/maister.git maister`)
  727. process.exit(1)
  728. }
  729. mkdirSync(OUT, { recursive: true })
  730. const skills = generateSkills()
  731. const agents = generateAgents()
  732. const commands = generateCommands()
  733. console.log(`maister v${VERSION} -> opencode`)
  734. console.log(` ${skills} skills -> .opencode/skill/${PREFIX}*/`)
  735. console.log(` ${agents} agents -> .opencode/agent/${PREFIX}*.md`)
  736. console.log(` ${commands} commands -> .opencode/command/${PREFIX}*.md`)
  737. console.log(`\nRestart opencode to load them.`)