this repo has no description

initial commit

+1120
+1
.gitignore
··· 1 + node_modules
+287
LICENSE.txt
··· 1 + EUROPEAN UNION PUBLIC LICENCE v. 1.2 2 + EUPL © the European Union 2007, 2016 3 + 4 + This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined 5 + below) which is provided under the terms of this Licence. Any use of the Work, 6 + other than as authorised under this Licence is prohibited (to the extent such 7 + use is covered by a right of the copyright holder of the Work). 8 + 9 + The Work is provided under the terms of this Licence when the Licensor (as 10 + defined below) has placed the following notice immediately following the 11 + copyright notice for the Work: 12 + 13 + Licensed under the EUPL 14 + 15 + or has expressed by any other means his willingness to license under the EUPL. 16 + 17 + 1. Definitions 18 + 19 + In this Licence, the following terms have the following meaning: 20 + 21 + - ‘The Licence’: this Licence. 22 + 23 + - ‘The Original Work’: the work or software distributed or communicated by the 24 + Licensor under this Licence, available as Source Code and also as Executable 25 + Code as the case may be. 26 + 27 + - ‘Derivative Works’: the works or software that could be created by the 28 + Licensee, based upon the Original Work or modifications thereof. This Licence 29 + does not define the extent of modification or dependence on the Original Work 30 + required in order to classify a work as a Derivative Work; this extent is 31 + determined by copyright law applicable in the country mentioned in Article 15. 32 + 33 + - ‘The Work’: the Original Work or its Derivative Works. 34 + 35 + - ‘The Source Code’: the human-readable form of the Work which is the most 36 + convenient for people to study and modify. 37 + 38 + - ‘The Executable Code’: any code which has generally been compiled and which is 39 + meant to be interpreted by a computer as a program. 40 + 41 + - ‘The Licensor’: the natural or legal person that distributes or communicates 42 + the Work under the Licence. 43 + 44 + - ‘Contributor(s)’: any natural or legal person who modifies the Work under the 45 + Licence, or otherwise contributes to the creation of a Derivative Work. 46 + 47 + - ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of 48 + the Work under the terms of the Licence. 49 + 50 + - ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, 51 + renting, distributing, communicating, transmitting, or otherwise making 52 + available, online or offline, copies of the Work or providing access to its 53 + essential functionalities at the disposal of any other natural or legal 54 + person. 55 + 56 + 2. Scope of the rights granted by the Licence 57 + 58 + The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 59 + sublicensable licence to do the following, for the duration of copyright vested 60 + in the Original Work: 61 + 62 + - use the Work in any circumstance and for all usage, 63 + - reproduce the Work, 64 + - modify the Work, and make Derivative Works based upon the Work, 65 + - communicate to the public, including the right to make available or display 66 + the Work or copies thereof to the public and perform publicly, as the case may 67 + be, the Work, 68 + - distribute the Work or copies thereof, 69 + - lend and rent the Work or copies thereof, 70 + - sublicense rights in the Work or copies thereof. 71 + 72 + Those rights can be exercised on any media, supports and formats, whether now 73 + known or later invented, as far as the applicable law permits so. 74 + 75 + In the countries where moral rights apply, the Licensor waives his right to 76 + exercise his moral right to the extent allowed by law in order to make effective 77 + the licence of the economic rights here above listed. 78 + 79 + The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to 80 + any patents held by the Licensor, to the extent necessary to make use of the 81 + rights granted on the Work under this Licence. 82 + 83 + 3. Communication of the Source Code 84 + 85 + The Licensor may provide the Work either in its Source Code form, or as 86 + Executable Code. If the Work is provided as Executable Code, the Licensor 87 + provides in addition a machine-readable copy of the Source Code of the Work 88 + along with each copy of the Work that the Licensor distributes or indicates, in 89 + a notice following the copyright notice attached to the Work, a repository where 90 + the Source Code is easily and freely accessible for as long as the Licensor 91 + continues to distribute or communicate the Work. 92 + 93 + 4. Limitations on copyright 94 + 95 + Nothing in this Licence is intended to deprive the Licensee of the benefits from 96 + any exception or limitation to the exclusive rights of the rights owners in the 97 + Work, of the exhaustion of those rights or of other applicable limitations 98 + thereto. 99 + 100 + 5. Obligations of the Licensee 101 + 102 + The grant of the rights mentioned above is subject to some restrictions and 103 + obligations imposed on the Licensee. Those obligations are the following: 104 + 105 + Attribution right: The Licensee shall keep intact all copyright, patent or 106 + trademarks notices and all notices that refer to the Licence and to the 107 + disclaimer of warranties. The Licensee must include a copy of such notices and a 108 + copy of the Licence with every copy of the Work he/she distributes or 109 + communicates. The Licensee must cause any Derivative Work to carry prominent 110 + notices stating that the Work has been modified and the date of modification. 111 + 112 + Copyleft clause: If the Licensee distributes or communicates copies of the 113 + Original Works or Derivative Works, this Distribution or Communication will be 114 + done under the terms of this Licence or of a later version of this Licence 115 + unless the Original Work is expressly distributed only under this version of the 116 + Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee 117 + (becoming Licensor) cannot offer or impose any additional terms or conditions on 118 + the Work or Derivative Work that alter or restrict the terms of the Licence. 119 + 120 + Compatibility clause: If the Licensee Distributes or Communicates Derivative 121 + Works or copies thereof based upon both the Work and another work licensed under 122 + a Compatible Licence, this Distribution or Communication can be done under the 123 + terms of this Compatible Licence. For the sake of this clause, ‘Compatible 124 + Licence’ refers to the licences listed in the appendix attached to this Licence. 125 + Should the Licensee's obligations under the Compatible Licence conflict with 126 + his/her obligations under this Licence, the obligations of the Compatible 127 + Licence shall prevail. 128 + 129 + Provision of Source Code: When distributing or communicating copies of the Work, 130 + the Licensee will provide a machine-readable copy of the Source Code or indicate 131 + a repository where this Source will be easily and freely available for as long 132 + as the Licensee continues to distribute or communicate the Work. 133 + 134 + Legal Protection: This Licence does not grant permission to use the trade names, 135 + trademarks, service marks, or names of the Licensor, except as required for 136 + reasonable and customary use in describing the origin of the Work and 137 + reproducing the content of the copyright notice. 138 + 139 + 6. Chain of Authorship 140 + 141 + The original Licensor warrants that the copyright in the Original Work granted 142 + hereunder is owned by him/her or licensed to him/her and that he/she has the 143 + power and authority to grant the Licence. 144 + 145 + Each Contributor warrants that the copyright in the modifications he/she brings 146 + to the Work are owned by him/her or licensed to him/her and that he/she has the 147 + power and authority to grant the Licence. 148 + 149 + Each time You accept the Licence, the original Licensor and subsequent 150 + Contributors grant You a licence to their contributions to the Work, under the 151 + terms of this Licence. 152 + 153 + 7. Disclaimer of Warranty 154 + 155 + The Work is a work in progress, which is continuously improved by numerous 156 + Contributors. It is not a finished work and may therefore contain defects or 157 + ‘bugs’ inherent to this type of development. 158 + 159 + For the above reason, the Work is provided under the Licence on an ‘as is’ basis 160 + and without warranties of any kind concerning the Work, including without 161 + limitation merchantability, fitness for a particular purpose, absence of defects 162 + or errors, accuracy, non-infringement of intellectual property rights other than 163 + copyright as stated in Article 6 of this Licence. 164 + 165 + This disclaimer of warranty is an essential part of the Licence and a condition 166 + for the grant of any rights to the Work. 167 + 168 + 8. Disclaimer of Liability 169 + 170 + Except in the cases of wilful misconduct or damages directly caused to natural 171 + persons, the Licensor will in no event be liable for any direct or indirect, 172 + material or moral, damages of any kind, arising out of the Licence or of the use 173 + of the Work, including without limitation, damages for loss of goodwill, work 174 + stoppage, computer failure or malfunction, loss of data or any commercial 175 + damage, even if the Licensor has been advised of the possibility of such damage. 176 + However, the Licensor will be liable under statutory product liability laws as 177 + far such laws apply to the Work. 178 + 179 + 9. Additional agreements 180 + 181 + While distributing the Work, You may choose to conclude an additional agreement, 182 + defining obligations or services consistent with this Licence. However, if 183 + accepting obligations, You may act only on your own behalf and on your sole 184 + responsibility, not on behalf of the original Licensor or any other Contributor, 185 + and only if You agree to indemnify, defend, and hold each Contributor harmless 186 + for any liability incurred by, or claims asserted against such Contributor by 187 + the fact You have accepted any warranty or additional liability. 188 + 189 + 10. Acceptance of the Licence 190 + 191 + The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ 192 + placed under the bottom of a window displaying the text of this Licence or by 193 + affirming consent in any other similar way, in accordance with the rules of 194 + applicable law. Clicking on that icon indicates your clear and irrevocable 195 + acceptance of this Licence and all of its terms and conditions. 196 + 197 + Similarly, you irrevocably accept this Licence and all of its terms and 198 + conditions by exercising any rights granted to You by Article 2 of this Licence, 199 + such as the use of the Work, the creation by You of a Derivative Work or the 200 + Distribution or Communication by You of the Work or copies thereof. 201 + 202 + 11. Information to the public 203 + 204 + In case of any Distribution or Communication of the Work by means of electronic 205 + communication by You (for example, by offering to download the Work from a 206 + remote location) the distribution channel or media (for example, a website) must 207 + at least provide to the public the information requested by the applicable law 208 + regarding the Licensor, the Licence and the way it may be accessible, concluded, 209 + stored and reproduced by the Licensee. 210 + 211 + 12. Termination of the Licence 212 + 213 + The Licence and the rights granted hereunder will terminate automatically upon 214 + any breach by the Licensee of the terms of the Licence. 215 + 216 + Such a termination will not terminate the licences of any person who has 217 + received the Work from the Licensee under the Licence, provided such persons 218 + remain in full compliance with the Licence. 219 + 220 + 13. Miscellaneous 221 + 222 + Without prejudice of Article 9 above, the Licence represents the complete 223 + agreement between the Parties as to the Work. 224 + 225 + If any provision of the Licence is invalid or unenforceable under applicable 226 + law, this will not affect the validity or enforceability of the Licence as a 227 + whole. Such provision will be construed or reformed so as necessary to make it 228 + valid and enforceable. 229 + 230 + The European Commission may publish other linguistic versions or new versions of 231 + this Licence or updated versions of the Appendix, so far this is required and 232 + reasonable, without reducing the scope of the rights granted by the Licence. New 233 + versions of the Licence will be published with a unique version number. 234 + 235 + All linguistic versions of this Licence, approved by the European Commission, 236 + have identical value. Parties can take advantage of the linguistic version of 237 + their choice. 238 + 239 + 14. Jurisdiction 240 + 241 + Without prejudice to specific agreement between parties, 242 + 243 + - any litigation resulting from the interpretation of this License, arising 244 + between the European Union institutions, bodies, offices or agencies, as a 245 + Licensor, and any Licensee, will be subject to the jurisdiction of the Court 246 + of Justice of the European Union, as laid down in article 272 of the Treaty on 247 + the Functioning of the European Union, 248 + 249 + - any litigation arising between other parties and resulting from the 250 + interpretation of this License, will be subject to the exclusive jurisdiction 251 + of the competent court where the Licensor resides or conducts its primary 252 + business. 253 + 254 + 15. Applicable Law 255 + 256 + Without prejudice to specific agreement between parties, 257 + 258 + - this Licence shall be governed by the law of the European Union Member State 259 + where the Licensor has his seat, resides or has his registered office, 260 + 261 + - this licence shall be governed by Belgian law if the Licensor has no seat, 262 + residence or registered office inside a European Union Member State. 263 + 264 + Appendix 265 + 266 + ‘Compatible Licences’ according to Article 5 EUPL are: 267 + 268 + - GNU General Public License (GPL) v. 2, v. 3 269 + - GNU Affero General Public License (AGPL) v. 3 270 + - Open Software License (OSL) v. 2.1, v. 3.0 271 + - Eclipse Public License (EPL) v. 1.0 272 + - CeCILL v. 2.0, v. 2.1 273 + - Mozilla Public Licence (MPL) v. 2 274 + - GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 275 + - Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for 276 + works other than software 277 + - European Union Public Licence (EUPL) v. 1.1, v. 1.2 278 + - Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong 279 + Reciprocity (LiLiQ-R+). 280 + 281 + The European Commission may update this Appendix to later versions of the above 282 + licences without producing a new version of the EUPL, as long as they provide 283 + the rights granted in Article 2 of this Licence and protect the covered Source 284 + Code from exclusive appropriation. 285 + 286 + All other changes or additions to this Appendix require the production of a new 287 + EUPL version.
+50
README.md
··· 1 + # langrules-opencode 2 + 3 + OpenCode plugin that provides language-specific coding rules and style guidelines. 4 + 5 + ## Installation 6 + 7 + Add to your `opencode.json`: 8 + 9 + ```json 10 + { 11 + "plugin": ["git@tangled.org:karitham.dev/langrules-opencode"] 12 + } 13 + ``` 14 + 15 + ## What it does 16 + 17 + This plugin provides a `langrules` tool that reads language-specific coding rules from `.rules/*.md` files in your project. It helps maintain consistent code style and best practices across your codebase. 18 + 19 + ### Features 20 + 21 + - **`langrules` tool**: Reads coding rules for specific languages (e.g., `langrules({ language: 'typescript' })`) 22 + - **Smart hints**: Automatically suggests reading relevant rules when you're about to edit a file 23 + - **Session tracking**: Only prompts once per language per session to avoid redundancy 24 + - **Custom rules**: Supports project-local rules in `.rules/` directory 25 + - **Global fallback**: Falls back to `$LANGRULES_DIR` for shared rule sets 26 + 27 + ### Supported Languages 28 + 29 + TypeScript, JavaScript, Go, Nix, Rust, Ruby, Python, Java, C, C++, C#, Shell, HTML, CSS, JSON, YAML, XML, TOML, Markdown, SQL 30 + 31 + ## Usage 32 + 33 + The plugin works automatically - when you edit a file, it suggests reading the relevant language rules. You can also manually invoke: 34 + 35 + ``` 36 + langrules({ language: 'typescript' }) 37 + ``` 38 + 39 + ## Rule Files 40 + 41 + Place language rule files in `.rules/` at your project root: 42 + 43 + ``` 44 + .rules/ 45 + typescript.md 46 + go.md 47 + python.md 48 + ``` 49 + 50 + Or set `LANGRULES_DIR` environment variable for global rules.
+35
bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "configVersion": 1, 4 + "workspaces": { 5 + "": { 6 + "name": "chainlink-opencode", 7 + "dependencies": { 8 + "@opencode-ai/plugin": "^1.1.47", 9 + }, 10 + "devDependencies": { 11 + "@types/bun": "latest", 12 + }, 13 + "peerDependencies": { 14 + "typescript": "^5", 15 + }, 16 + }, 17 + }, 18 + "packages": { 19 + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.1.51", "", { "dependencies": { "@opencode-ai/sdk": "1.1.51", "zod": "4.1.8" } }, "sha512-FMtwIEG1HdXaQ4qtzRelF++qjKL4QKtJOB5Atud0Xu5c9T48TGCDDQJONTAjgVleyq3bb73tUt+ACBK0QSEOyw=="], 20 + 21 + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.51", "", {}, "sha512-UL66X46AGgui5xURGEenXsIsgNVmgfkmJJeRtdeOMLhi/RcbTBikfPjjtmym3VLnqp855Wt7dZ/vAjOXqiUKXA=="], 22 + 23 + "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], 24 + 25 + "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], 26 + 27 + "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="], 28 + 29 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 30 + 31 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 32 + 33 + "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], 34 + } 35 + }
+462
docs/opencode-plugin-architecture.md
··· 1 + # OpenCode Plugin Architecture 2 + 3 + Documentation for agents implementing OpenCode plugins. Based on analysis of [kdco/opencode-workspace](https://github.com/kdcokenny/opencode-workspace). 4 + 5 + ## Quick Reference 6 + 7 + ```typescript 8 + import { type Plugin, type Event, tool } from "@opencode-ai/plugin"; 9 + 10 + export const MyPlugin: Plugin = async ({ $, client, directory }) => { 11 + return { 12 + tool: { 13 + myTool: tool({ 14 + description: "What this tool does", 15 + args: { 16 + param: tool.schema.string().describe("Parameter description"), 17 + }, 18 + async execute(args) { 19 + // Tools return string output 20 + return "result"; 21 + }, 22 + }), 23 + }, 24 + "session.created": async ({ sessionID }: { sessionID: string }) => { ... }, 25 + event: async ({ event }: { event: Event }) => { ... }, 26 + }; 27 + }; 28 + ``` 29 + 30 + ## Plugin Structure 31 + 32 + ### Plugin Function Signature 33 + 34 + ```typescript 35 + export const MyPlugin: Plugin = async (ctx) => { 36 + const { $, client, directory, worktree } = ctx; 37 + // ctx includes all plugin context 38 + }; 39 + ``` 40 + 41 + **Context properties:** 42 + 43 + - `$`: Bun shell API for executing commands 44 + - `client`: OpenCode SDK client for API interactions 45 + - `directory`: Current working directory 46 + - `worktree`: Git worktree path 47 + 48 + ### Tool Definition 49 + 50 + ```typescript 51 + tool({ 52 + description: "Human-readable description of the tool", 53 + args: { 54 + paramName: tool.schema 55 + .string() // string, number, boolean 56 + .describe("What this parameter means"), 57 + optionalParam: tool.schema 58 + .string() 59 + .optional() 60 + .describe("Optional parameter"), 61 + enumParam: tool.schema 62 + .enum(["option1", "option2"]) 63 + .describe("Enumerated value"), 64 + recordParam: tool.schema 65 + .record(tool.schema.string(), tool.schema.unknown()) 66 + .optional() 67 + .describe("Key-value pairs"), 68 + }, 69 + async execute(args) { 70 + const { paramName, optionalParam } = args; 71 + return "tool output as string"; 72 + }, 73 + }); 74 + ``` 75 + 76 + **Schema types:** 77 + 78 + - `tool.schema.string()` 79 + - `tool.schema.number()` 80 + - `tool.schema.boolean()` 81 + - `tool.schema.enum(["a", "b"])` 82 + - `tool.schema.record(keyType, valueType)` 83 + - `.optional()` 84 + - `.describe()` 85 + - `.default(value)` 86 + 87 + ### Returning Values from Tools 88 + 89 + Tools must return a **string**. For structured data, return JSON: 90 + 91 + ```typescript 92 + async execute(args) { 93 + const result = { status: "success", data: {...} }; 94 + return JSON.stringify(result); 95 + } 96 + ``` 97 + 98 + ## Lifecycle Hooks 99 + 100 + ### session.created 101 + 102 + Called when a new session starts. Use to inject context. 103 + 104 + ```typescript 105 + "session.created": async ({ sessionID }: { sessionID: string }) => { 106 + await client.session.prompt({ 107 + path: { id: sessionID }, 108 + body: { 109 + noReply: true, 110 + parts: [{ type: "text", text: "Injected context" }], 111 + }, 112 + }); 113 + } 114 + ``` 115 + 116 + ### event 117 + 118 + All other events are dispatched through the generic `event` hook. Filter by `event.type`. 119 + 120 + ```typescript 121 + event: async ({ event }: { event: Event }) => { 122 + if (event.type !== "session.idle") return; 123 + if (!event.sessionID) return; 124 + 125 + // Handle session.idle 126 + }; 127 + ``` 128 + 129 + **Common event types:** 130 + 131 + - `session.created` 132 + - `session.idle` 133 + - `session.compacted` 134 + - `session.deleted` 135 + - `session.diff` 136 + - `session.error` 137 + - `session.status` 138 + - `session.updated` 139 + - `tool.execute.before` 140 + - `tool.execute.after` 141 + - `chat.message` 142 + - `command.execute.before` 143 + 144 + ## Client API 145 + 146 + ### Logging 147 + 148 + ```typescript 149 + // Structured logging 150 + await client.app 151 + .log({ 152 + body: { 153 + service: "my-plugin", 154 + level: "debug" | "info" | "warn" | "error", 155 + message: "log message", 156 + }, 157 + }) 158 + .catch(() => {}); // Always handle errors 159 + ``` 160 + 161 + ### Session Operations 162 + 163 + ```typescript 164 + // Inject context without triggering AI response 165 + await client.session.prompt({ 166 + path: { id: sessionID }, 167 + body: { 168 + noReply: true, 169 + parts: [{ type: "text", text: "context" }], 170 + }, 171 + }); 172 + 173 + // Fork a session 174 + const forked = await client.session.fork({ 175 + path: { id: sessionID }, 176 + body: {}, 177 + }); 178 + 179 + // Get session info 180 + const session = await client.session.get({ 181 + path: { id: sessionID }, 182 + }); 183 + 184 + // Delete a session 185 + await client.session.delete({ path: { id: sessionID } }); 186 + ``` 187 + 188 + ### Tool Context in Hooks 189 + 190 + Tool hooks receive a `toolCtx` parameter: 191 + 192 + ```typescript 193 + async execute(args, toolCtx) { 194 + // toolCtx includes: 195 + // - sessionID: string | undefined 196 + // - directory: string 197 + // - worktree: string 198 + const sessionID = toolCtx?.sessionID; 199 + } 200 + ``` 201 + 202 + ## Shell Execution 203 + 204 + Use Bun's `$` template literal syntax: 205 + 206 + ```typescript 207 + // Simple command 208 + const output = await $`echo hello`.text(); 209 + 210 + // With arguments (properly escaped) 211 + const output = await $`command "${arg.replace(/"/g, '\\"')}"`.text(); 212 + 213 + // Capture output 214 + const result = await $`ls -la`.text(); 215 + 216 + // Error handling 217 + try { 218 + await $`command`.text(); 219 + } catch (error) { 220 + // Command failed 221 + } 222 + ``` 223 + 224 + ## TypeScript Patterns 225 + 226 + ### Result Types 227 + 228 + ```typescript 229 + type Result<T> = 230 + | { ok: true; value: T } 231 + | { ok: false; error: string }; 232 + 233 + // Usage 234 + const result: Result<string> = ...; 235 + if (result.ok) { 236 + console.log(result.value); 237 + } else { 238 + console.error(result.error); 239 + } 240 + ``` 241 + 242 + ### Zod Schema Validation 243 + 244 + Example from workspace-plugin: 245 + 246 + ```typescript 247 + import { z } from "zod"; 248 + 249 + const PlanSchema = z.object({ 250 + frontmatter: z.object({ 251 + status: z.enum(["not-started", "in-progress", "complete", "blocked"]), 252 + phase: z.number().int().positive(), 253 + }), 254 + goal: z.string().min(10), 255 + }); 256 + 257 + // Validate at boundary 258 + const result = PlanSchema.safeParse(candidate); 259 + if (!result.success) { 260 + return `Error: ${result.error.message}`; 261 + } 262 + ``` 263 + 264 + ### Error Handling 265 + 266 + ```typescript 267 + // Type guard for Node.js errors 268 + function isNodeError(error: unknown): error is NodeJS.ErrnoException { 269 + return error instanceof Error && "code" in error; 270 + } 271 + 272 + // Usage 273 + try { 274 + await fs.readFile(path); 275 + } catch (error) { 276 + if (isNodeError(error) && error.code === "ENOENT") { 277 + return "File not found"; 278 + } 279 + throw error; // Re-throw unexpected errors 280 + } 281 + ``` 282 + 283 + ## Common Operations 284 + 285 + ### Reading Files 286 + 287 + ```typescript 288 + import * as fs from "node:fs/promises"; 289 + 290 + async function readFile(filePath: string): Promise<string> { 291 + try { 292 + return await fs.readFile(filePath, "utf8"); 293 + } catch (error) { 294 + if (isNodeError(error) && error.code === "ENOENT") { 295 + return ""; // File doesn't exist 296 + } 297 + throw error; 298 + } 299 + } 300 + ``` 301 + 302 + ### Writing Files 303 + 304 + ```typescript 305 + import * as fs from "node:fs/promises"; 306 + 307 + async function writeFile(filePath: string, content: string): Promise<void> { 308 + await fs.mkdir(path.dirname(filePath), { recursive: true }); 309 + await fs.writeFile(filePath, content, "utf8"); 310 + } 311 + ``` 312 + 313 + ### Path Handling 314 + 315 + ```typescript 316 + import * as path from "node:path"; 317 + 318 + // Join paths 319 + const fullPath = path.join(directory, "subdir", "file.txt"); 320 + 321 + // Get directory name 322 + const dir = path.dirname(filePath); 323 + 324 + // Get base name 325 + const base = path.basename(filePath); 326 + 327 + // Get extension 328 + const ext = path.extname(filePath); 329 + ``` 330 + 331 + ### Git Operations 332 + 333 + ```typescript 334 + async function git( 335 + args: string[], 336 + cwd: string, 337 + ): Promise<Result<string, string>> { 338 + try { 339 + const proc = Bun.spawn(["git", ...args], { 340 + cwd, 341 + stdout: "pipe", 342 + stderr: "pipe", 343 + }); 344 + const [stdout, stderr, exitCode] = await Promise.all([ 345 + new Response(proc.stdout).text(), 346 + new Response(proc.stderr).text(), 347 + proc.exited, 348 + ]); 349 + if (exitCode !== 0) { 350 + return { ok: false, error: stderr.trim() }; 351 + } 352 + return { ok: true, value: stdout.trim() }; 353 + } catch (error) { 354 + return { ok: false, error: String(error) }; 355 + } 356 + } 357 + ``` 358 + 359 + ## Best Practices 360 + 361 + ### 1. Early Exit Pattern 362 + 363 + ```typescript 364 + async function process(args: Args): Promise<Result> { 365 + // Validate first 366 + if (!args.required) { 367 + return { ok: false, error: "Required parameter missing" }; 368 + } 369 + 370 + // Guard against null/undefined 371 + if (!value) return defaultValue; 372 + 373 + // Happy path 374 + return { ok: true, value: computed }; 375 + } 376 + ``` 377 + 378 + ### 2. Parse Don't Validate 379 + 380 + Extract data first, validate once: 381 + 382 + ```typescript 383 + function extractMarkdownParts(content: string): RawParts { 384 + // Extraction only, no validation 385 + const match = content.match(/pattern/); 386 + return { data: match?.[1] || null }; 387 + } 388 + 389 + function parsePlan(content: string): ValidPlan { 390 + const parts = extractMarkdownParts(content); 391 + const result = schema.safeParse(parts); 392 + if (!result.success) { 393 + throw new Error("Invalid format"); 394 + } 395 + return result.data; 396 + } 397 + ``` 398 + 399 + ### 3. Fail Loud 400 + 401 + Provide actionable error messages: 402 + 403 + ```typescript 404 + async function execute(args) { 405 + const result = await riskyOperation(); 406 + if (!result.ok) { 407 + return `❌ Operation failed: ${result.error}\n\nHint: Check that X is configured correctly.`; 408 + } 409 + return "✅ Success!"; 410 + } 411 + ``` 412 + 413 + ### 4. Log with Context 414 + 415 + ```typescript 416 + async function log(level: "info" | "error", message: string) { 417 + await client.app 418 + .log({ 419 + body: { 420 + service: "my-plugin", 421 + level, 422 + message: `${message} (session: ${sessionID})`, 423 + }, 424 + }) 425 + .catch(() => {}); 426 + } 427 + ``` 428 + 429 + ### 5. Handle Promise Rejection 430 + 431 + Logging and async operations can reject: 432 + 433 + ```typescript 434 + await client.app.log({...}).catch(() => {}); 435 + 436 + // Or handle gracefully 437 + try { 438 + await client.session.prompt({...}); 439 + } catch (error) { 440 + await log("error", `Failed: ${error}`); 441 + } 442 + ``` 443 + 444 + ## File Structure 445 + 446 + ``` 447 + my-plugin/ 448 + ├── src/ 449 + │ └── index.ts # Main plugin file 450 + ├── package.json # Dependencies 451 + ├── tsconfig.json # TypeScript config 452 + └── docs/ 453 + └── opencode-plugin-architecture.md # This file 454 + ``` 455 + 456 + ## References 457 + 458 + - [OpenCode Plugins Documentation](https://opencode.ai/docs/plugins) 459 + - [OpenCode SDK](https://opencode.ai/docs/sdk) 460 + - [kdco/opencode-workspace](https://github.com/kdcokenny/opencode-workspace) 461 + - [Bun Shell API](https://bun.com/docs/runtime/shell) 462 + - [Zod Documentation](https://zod.dev/)
+48
flake.lock
··· 1 + { 2 + "nodes": { 3 + "flake-parts": { 4 + "inputs": { 5 + "nixpkgs-lib": [ 6 + "nixpkgs" 7 + ] 8 + }, 9 + "locked": { 10 + "lastModified": 1769996383, 11 + "narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=", 12 + "owner": "hercules-ci", 13 + "repo": "flake-parts", 14 + "rev": "57928607ea566b5db3ad13af0e57e921e6b12381", 15 + "type": "github" 16 + }, 17 + "original": { 18 + "owner": "hercules-ci", 19 + "repo": "flake-parts", 20 + "type": "github" 21 + } 22 + }, 23 + "nixpkgs": { 24 + "locked": { 25 + "lastModified": 1770115704, 26 + "narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=", 27 + "owner": "NixOS", 28 + "repo": "nixpkgs", 29 + "rev": "e6eae2ee2110f3d31110d5c222cd395303343b08", 30 + "type": "github" 31 + }, 32 + "original": { 33 + "owner": "NixOS", 34 + "ref": "nixos-unstable", 35 + "repo": "nixpkgs", 36 + "type": "github" 37 + } 38 + }, 39 + "root": { 40 + "inputs": { 41 + "flake-parts": "flake-parts", 42 + "nixpkgs": "nixpkgs" 43 + } 44 + } 45 + }, 46 + "root": "root", 47 + "version": 7 48 + }
+27
flake.nix
··· 1 + { 2 + inputs = { 3 + flake-parts.url = "github:hercules-ci/flake-parts"; 4 + flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; 5 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 + }; 7 + outputs = 8 + inputs@{ flake-parts, ... }: 9 + flake-parts.lib.mkFlake { inherit inputs; } { 10 + systems = [ 11 + "x86_64-linux" 12 + "aarch64-linux" 13 + "aarch64-darwin" 14 + "x86_64-darwin" 15 + ]; 16 + perSystem = 17 + { pkgs, ... }: 18 + { 19 + devShells.default = pkgs.mkShell { 20 + buildInputs = [ 21 + pkgs.bun 22 + pkgs.biome 23 + ]; 24 + }; 25 + }; 26 + }; 27 + }
+14
package.json
··· 1 + { 2 + "name": "chainlink-opencode", 3 + "private": true, 4 + "main": "src/index.ts", 5 + "devDependencies": { 6 + "@types/bun": "latest" 7 + }, 8 + "peerDependencies": { 9 + "typescript": "^5" 10 + }, 11 + "dependencies": { 12 + "@opencode-ai/plugin": "^1.1.47" 13 + } 14 + }
+167
src/index.ts
··· 1 + import { readFileSync } from "node:fs"; 2 + import { 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 + */ 8 + const 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 + */ 78 + function 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 + */ 88 + const sessionRulesRead = new Map<string, Set<string>>(); 89 + 90 + export 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 + };
+29
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 + 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 + 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 + "noImplicitOverride": true, 23 + 24 + // Some stricter flags (disabled by default) 25 + "noUnusedLocals": false, 26 + "noUnusedParameters": false, 27 + "noPropertyAccessFromIndexSignature": false 28 + } 29 + }