this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 132bde3836006f78d1cde42248fa8a5c7fa149fd 167 lines 3.5 kB view raw
1import { readFileSync } from "node:fs"; 2import { type Plugin, tool } from "@opencode-ai/plugin"; 3 4/** 5 * Extension to language mapping for rule recommendations. 6 * Only includes languages we have rule files for. 7 */ 8const EXTENSION_TO_LANGUAGE: Record<string, string> = { 9 // TypeScript/JavaScript 10 ts: "typescript", 11 tsx: "typescript", 12 js: "javascript", 13 jsx: "javascript", 14 mjs: "javascript", 15 cjs: "javascript", 16 17 // Go 18 go: "go", 19 20 // Nix 21 nix: "nix", 22 23 // Rust 24 rs: "rust", 25 26 // Ruby 27 rb: "ruby", 28 erb: "ruby", 29 rake: "ruby", 30 31 // Python 32 py: "python", 33 pyw: "python", 34 pyi: "python", 35 36 // Java/C-family 37 java: "java", 38 c: "c", 39 h: "c", 40 cpp: "cpp", 41 cc: "cpp", 42 cxx: "cpp", 43 hpp: "cpp", 44 cs: "csharp", 45 46 // Shell 47 sh: "shell", 48 bash: "shell", 49 zsh: "shell", 50 51 // Web 52 html: "html", 53 htm: "html", 54 css: "css", 55 scss: "css", 56 sass: "css", 57 less: "css", 58 59 // Data/Config 60 json: "json", 61 yaml: "yaml", 62 yml: "yaml", 63 xml: "xml", 64 toml: "toml", 65 66 // Documentation 67 md: "markdown", 68 markdown: "markdown", 69 70 // Database 71 sql: "sql", 72 ddl: "sql", 73}; 74 75/** 76 * Extract file extension and map to language name. 77 */ 78function getLanguageFromFilePath(filePath: string): string | null { 79 const match = filePath.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/); 80 if (!match) return null; 81 const ext = match[1]!.toLowerCase(); 82 return EXTENSION_TO_LANGUAGE[ext] ?? null; 83} 84 85/** 86 * Registry to track which rules have been read in each session. 87 */ 88const sessionRulesRead = new Map<string, Set<string>>(); 89 90export const LangRulesPlugin: Plugin = async ({ client, directory }) => { 91 return { 92 tool: { 93 langrules: tool({ 94 description: 95 "Read the critical rules needed for style and necessary context", 96 args: { 97 language: tool.schema 98 .string() 99 .describe( 100 "Specific rule file to read (e.g., 'typescript', 'go', 'javascript')", 101 ), 102 }, 103 async execute(args, toolCtx) { 104 const sessionID = toolCtx?.sessionID; 105 const languageFile = args.language; 106 107 if (sessionID) { 108 if (!sessionRulesRead.has(sessionID)) { 109 sessionRulesRead.set(sessionID, new Set()); 110 } 111 112 sessionRulesRead.get(sessionID)?.add(languageFile); 113 } 114 115 const paths = [`${directory}/.rules/${languageFile}.md`]; 116 117 const rulesDir = process.env.LANGRULES_DIR; 118 if (rulesDir) { 119 paths.push(`${rulesDir}/${languageFile}.md`); 120 } 121 122 for (const rulePath of paths) { 123 try { 124 const content = readFileSync(rulePath).toString(); 125 if (content.trim()) return content.trim(); 126 } catch {} 127 } 128 129 return `No rule file found for '${languageFile}'`; 130 }, 131 }), 132 }, 133 134 "tool.execute.before": async (input) => { 135 const { sessionID, args } = input as { 136 tool: string; 137 sessionID: string; 138 callID: string; 139 args?: Record<string, unknown>; 140 }; 141 142 if (!sessionID) return; 143 144 const filePath = (args?.filePath as string) || (args?.path as string); 145 if (!filePath) return; 146 147 const language = getLanguageFromFilePath(filePath); 148 const ext = filePath.split(".").pop()?.toLowerCase() || ""; 149 150 if (language && !sessionRulesRead.get(sessionID)?.has(language)) 151 client.session.prompt({ 152 path: { 153 id: sessionID, 154 }, 155 body: { 156 parts: [ 157 { 158 synthetic: true, 159 type: "text", 160 text: `**Hint:** You're about to modify a \`${ext}\` file (${language}). Consider reading the ${language} coding rules: \`langrules({ language: '${language}' }})\``, 161 }, 162 ], 163 }, 164 }); 165 }, 166 }; 167};