convert-agents.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. #!/usr/bin/env node
  2. /**
  3. * convert-agents.js
  4. * Converts OpenAgents Control to Claude Code format
  5. *
  6. * Usage: node convert-agents.js [--watch]
  7. */
  8. const fs = require('fs');
  9. const path = require('path');
  10. // Configuration - Use absolute paths from the script location
  11. const SCRIPT_DIR = __dirname;
  12. const REPO_ROOT = path.resolve(path.join(SCRIPT_DIR, '../../../../'));
  13. const SOURCE_DIR = path.join(REPO_ROOT, '.opencode/agent');
  14. const OUTPUT_DIR = path.join(SCRIPT_DIR, '../generated');
  15. const CLAUDE_AGENTS_DIR = path.join(OUTPUT_DIR, 'agents');
  16. const CLAUDE_SKILLS_DIR = path.join(OUTPUT_DIR, 'skills');
  17. // Claude frontmatter fields (subset of OpenCode)
  18. const CLAUDE_FIELDS = ['name', 'description', 'tools', 'model', 'permissionMode', 'skills', 'hooks'];
  19. console.log('🚀 OpenAgents Control → Claude Code Converter');
  20. console.log(` Source: ${SOURCE_DIR}`);
  21. console.log(` Output: ${OUTPUT_DIR}\n`);
  22. /**
  23. * Parses YAML frontmatter from markdown
  24. */
  25. function parseFrontmatter(content) {
  26. const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
  27. if (!match) return { frontmatter: null, content };
  28. const yaml = match[1];
  29. const body = match[2];
  30. const frontmatter = {};
  31. yaml.split('\n').forEach(line => {
  32. const colonIndex = line.indexOf(':');
  33. if (colonIndex > -1) {
  34. const key = line.slice(0, colonIndex).trim();
  35. let value = line.slice(colonIndex + 1).trim();
  36. // Parse arrays
  37. if (value.startsWith('[') && value.endsWith(']')) {
  38. value = value.slice(1, -1).split(',').map(v => v.trim().replace(/"/g, ''));
  39. }
  40. frontmatter[key] = value;
  41. }
  42. });
  43. return { frontmatter, body };
  44. }
  45. /**
  46. * Converts OpenCode frontmatter to Claude format
  47. */
  48. function convertFrontmatter(ocFrontmatter) {
  49. const claude = {};
  50. // Map OpenCode fields to Claude fields
  51. claude.name = ocFrontmatter.id || ocFrontmatter.name;
  52. claude.description = ocFrontmatter.description;
  53. // Map tools from OpenCode permissions to Claude
  54. if (ocFrontmatter.tools) {
  55. claude.tools = ocFrontmatter.tools;
  56. } else if (ocFrontmatter.permissions) {
  57. // Extract allowed tools from permissions block
  58. const tools = [];
  59. if (ocFrontmatter.permissions.read) tools.push('Read');
  60. if (ocFrontmatter.permissions.grep) tools.push('Grep');
  61. if (ocFrontmatter.permissions.glob) tools.push('Glob');
  62. if (ocFrontmatter.permissions.edit) tools.push('Edit');
  63. if (ocFrontmatter.permissions.write) tools.push('Write');
  64. if (ocFrontmatter.permissions.bash) tools.push('Bash');
  65. claude.tools = tools.join(', ');
  66. }
  67. // Map model
  68. claude.model = mapModel(ocFrontmatter.model);
  69. // Map permissionMode (default to 'default' if not specified)
  70. claude.permissionMode = ocFrontmatter.mode === 'subagent' ? 'plan' : 'default';
  71. return claude;
  72. }
  73. /**
  74. * Maps OpenCode model names to Claude model aliases
  75. */
  76. function mapModel(model) {
  77. const modelMap = {
  78. 'opencode/grok-code': 'sonnet',
  79. 'opencode/grok': 'opus',
  80. 'gpt-4': 'sonnet',
  81. 'gpt-4o': 'sonnet',
  82. 'haiku': 'haiku',
  83. };
  84. return modelMap[model] || 'sonnet';
  85. }
  86. /**
  87. * Generates Claude markdown from converted data
  88. */
  89. function generateClaudeMarkdown(claudeFrontmatter, body) {
  90. const fm = Object.entries(claudeFrontmatter)
  91. .map(([key, value]) => {
  92. if (Array.isArray(value)) {
  93. return `${key}: [${value.map(v => `"${v}"`).join(', ')}]`;
  94. }
  95. return `${key}: "${value}"`;
  96. })
  97. .join('\n');
  98. return `---\n${fm}\n---\n\n${body}`;
  99. }
  100. /**
  101. * Recursively finds all .md files in a directory
  102. */
  103. function findMarkdownFiles(dir, files = []) {
  104. const entries = fs.readdirSync(dir, { withFileTypes: true });
  105. for (const entry of entries) {
  106. const fullPath = path.join(dir, entry.name);
  107. if (entry.isDirectory()) {
  108. findMarkdownFiles(fullPath, files);
  109. } else if (entry.name.endsWith('.md')) {
  110. files.push(fullPath);
  111. }
  112. }
  113. return files;
  114. }
  115. /**
  116. * Processes a single agent file
  117. */
  118. function processAgent(filePath) {
  119. const content = fs.readFileSync(filePath, 'utf8');
  120. const { frontmatter, body } = parseFrontmatter(content);
  121. if (!frontmatter) {
  122. console.log(`⚠️ Skipping ${filePath} (no frontmatter)`);
  123. return;
  124. }
  125. const claudeFrontmatter = convertFrontmatter(frontmatter);
  126. const claudeMarkdown = generateClaudeMarkdown(claudeFrontmatter, body);
  127. // Determine output path
  128. const relativePath = path.relative(SOURCE_DIR, filePath);
  129. const outputPath = path.join(CLAUDE_AGENTS_DIR, relativePath);
  130. // Ensure output directory exists
  131. fs.mkdirSync(path.dirname(outputPath), { recursive: true });
  132. fs.writeFileSync(outputPath, claudeMarkdown);
  133. console.log(`✅ Converted: ${relativePath}`);
  134. }
  135. /**
  136. * Main conversion function
  137. */
  138. function convert() {
  139. // Clean output directories
  140. if (fs.existsSync(CLAUDE_AGENTS_DIR)) fs.rmSync(CLAUDE_AGENTS_DIR, { recursive: true });
  141. if (fs.existsSync(CLAUDE_SKILLS_DIR)) fs.rmSync(CLAUDE_SKILLS_DIR, { recursive: true });
  142. fs.mkdirSync(CLAUDE_AGENTS_DIR, { recursive: true });
  143. fs.mkdirSync(path.join(CLAUDE_SKILLS_DIR, 'openagents-control-standards'), { recursive: true });
  144. console.log('📦 Converting agents...\n');
  145. // Process category agents
  146. const agentFiles = findMarkdownFiles(SOURCE_DIR);
  147. agentFiles.forEach(processAgent);
  148. // Create default context-scout subagent
  149. const contextScoutContent = `---
  150. name: context-scout
  151. description: Discovers and recommends OpenAgents Control context files using glob, read, and grep tools. Use when you need to find OpenAgents Control standards, guides, or domain knowledge in the .opencode/context directory.
  152. tools: Read, Grep, Glob
  153. model: haiku
  154. permissionMode: plan
  155. ---
  156. # ContextScout
  157. You discover and recommend relevant OpenAgents Control context files from \`.opencode/context/\` based on the user's request.
  158. ## Your Process
  159. 1. Use \`Glob\` to find files in \`.opencode/context/\`.
  160. 2. Use \`Read\` or \`Grep\` to verify relevance.
  161. 3. Return file paths with brief descriptions.
  162. `;
  163. fs.writeFileSync(
  164. path.join(CLAUDE_AGENTS_DIR, 'context-scout.md'),
  165. contextScoutContent
  166. );
  167. // Create openagents-control-standards skill
  168. const skillContent = `---
  169. name: openagents-control-standards
  170. description: Automatically triggers before any task to ensure OpenAgents Control standards and context are loaded. Use when the user asks to create, modify, or analyze anything in this repository.
  171. ---
  172. # OpenAgents Control Standards Loader
  173. Before proceeding with the user's request:
  174. 1. Call the \`context-scout\` subagent with the user's request to find relevant OpenAgents Control context files.
  175. 2. Read the returned "Critical" and "High" priority files.
  176. 3. Apply the OpenAgents Control standards found to your work.
  177. `;
  178. fs.writeFileSync(
  179. path.join(CLAUDE_SKILLS_DIR, 'openagents-control-standards/SKILL.md'),
  180. skillContent
  181. );
  182. console.log('\n✨ Conversion complete!');
  183. console.log(` Output: ${OUTPUT_DIR}`);
  184. }
  185. // Run conversion
  186. convert();