├── .dewhale ├── characters │ ├── Issac.yaml │ ├── Miles.yaml │ └── Taylor.yaml └── config.yaml ├── .github └── workflows │ ├── dewhale-trigger.yaml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── README.zh_CN.md ├── docs ├── CD.md ├── CD.zh_CN.md ├── architecture.md ├── architecture.zh_CN.md ├── quick-start.md └── quick-start.zh_CN.md ├── jsr.json ├── mcp-servers └── shadcn │ ├── code-refiner.ts │ ├── components.ts │ ├── index.ts │ ├── parser.ts │ └── system-prompts.ts ├── src ├── character.ts ├── entry.ts ├── lib │ ├── config.ts │ ├── llm.ts │ └── mcp.ts ├── platform │ ├── github.ts │ ├── gitlab.ts │ └── index.ts └── types.ts └── website ├── README.md ├── astro.config.mjs ├── package.json ├── public ├── bg.png └── logo.png ├── src ├── components │ └── ImgAndText.astro ├── env.d.ts ├── layouts │ └── Layout.astro └── pages │ └── index.astro ├── tsconfig.json └── yarn.lock /.dewhale/characters/Issac.yaml: -------------------------------------------------------------------------------- 1 | name: Issac 2 | labels: 3 | - "Issac-help" 4 | systemPrompt: | 5 | 你叫 Issac,是一个负责清理当前仓库历史遗留 Issue 的 AI 助手。 6 | llm: 7 | maxSteps: 20 8 | -------------------------------------------------------------------------------- /.dewhale/characters/Miles.yaml: -------------------------------------------------------------------------------- 1 | name: Miles 2 | labels: 3 | - "Miles-help" 4 | systemPrompt: | 5 | 你叫 Miles,负责在仓库中配置 GitOps,例如 GitHub 的 Actions,Gitlab 的 CI/CD 等。 6 | 在进行配置时,首先读取当前仓库已有的配置,再结合用户的需求,维护对应的配置文件。 7 | 最终使用 API 将修改以 PR 的形式提交。 8 | 9 | 一个常见的工作流如下: 10 | 1. 理解需求,计划实现方式 11 | 2. 确认是否有需要用户补充输入的信息 12 | 3. 读取当前仓库的配置文件,可以使用以下 tools 13 | - read_file 14 | - read_multiple_files 15 | - list_directory 16 | - list_allowed_directories 17 | 4. 根据需求实现出需要创建/修改的文件及内容(path and content) 18 | 5. 基于当前分支创建新的分支,可以使用以下 tools 19 | - create_branch 20 | 6. 将变更内容提交到新分支,可以使用以下 tools 21 | - push_files 22 | 7. 基于新分支创建 PR,可以使用以下 tools 23 | - create_pull_request 24 | llm: 25 | maxSteps: 10 26 | mcp: 27 | servers: 28 | - type: stdio 29 | command: npx 30 | args: 31 | - "-y" 32 | - "@modelcontextprotocol/server-github" 33 | env: 34 | GITHUB_PERSONAL_ACCESS_TOKEN: ${{ env_GITHUB_TOKEN }} 35 | tools: 36 | create_branch: {} 37 | create_pull_request: {} 38 | push_files: {} 39 | - type: stdio 40 | command: npx 41 | args: 42 | - "-y" 43 | - "jina-ai-mcp-server" 44 | env: 45 | JINA_API_KEY: ${{ env_JINA_API_KEY }} 46 | - type: stdio 47 | command: npx 48 | args: 49 | - "-y" 50 | - "@modelcontextprotocol/server-filesystem" 51 | - "${{ env_GITHUB_WORKSPACE }}" 52 | tools: 53 | read_file: {} 54 | read_multiple_files: {} 55 | list_directory: {} 56 | list_allowed_directories: {} -------------------------------------------------------------------------------- /.dewhale/characters/Taylor.yaml: -------------------------------------------------------------------------------- 1 | name: Taylor 2 | labels: 3 | - "Taylor-code-it" 4 | systemPrompt: | 5 | 你叫 Taylor,专门负责编写原型代码。 6 | 大家需要你时,通常会给你一个外部的参考,例如文档、示例代码,并给你一些要求,让你帮忙按照特定的编程规范编写示例代码。 7 | 最终结果如果用户有要求提交 PR,则提交 PR。否则以文字形式回复用户即可。 8 | 9 | 一个常见的工作流如下: 10 | 1. 理解需求,计划实现方式 11 | 2. 确认是否有需要用户补充输入的信息 12 | 3. 读取文件,可以使用以下 tools 13 | - read_file 14 | - read_multiple_files 15 | - list_directory 16 | - list_allowed_directories 17 | 4. 根据需求实现出需要创建/修改的文件及内容(path and content) 18 | 5. 基于当前分支创建新的分支,可以使用以下 tools 19 | - create_branch 20 | 6. 将变更内容提交到新分支,可以使用以下 tools 21 | - push_files 22 | 7. 基于新分支创建 PR,可以使用以下 tools 23 | - create_pull_request 24 | llm: 25 | maxSteps: 20 26 | mcp: 27 | servers: 28 | - type: stdio 29 | command: npx 30 | args: 31 | - "-y" 32 | - "@modelcontextprotocol/server-github" 33 | env: 34 | GITHUB_PERSONAL_ACCESS_TOKEN: ${{ env_GITHUB_TOKEN }} 35 | tools: 36 | create_branch: {} 37 | create_pull_request: {} 38 | push_files: {} 39 | - type: stdio 40 | command: npx 41 | args: 42 | - "-y" 43 | - "jina-ai-mcp-server" 44 | env: 45 | JINA_API_KEY: ${{ env_JINA_API_KEY }} 46 | - type: stdio 47 | command: npx 48 | args: 49 | - "-y" 50 | - "@modelcontextprotocol/server-filesystem" 51 | - "${{ env_GITHUB_WORKSPACE }}" 52 | tools: 53 | read_file: {} 54 | read_multiple_files: {} 55 | list_directory: {} 56 | list_allowed_directories: {} -------------------------------------------------------------------------------- /.dewhale/config.yaml: -------------------------------------------------------------------------------- 1 | llm: 2 | provider: google 3 | model: gemini-2.0-flash 4 | maxTokens: 8192 5 | maxSteps: 3 6 | maxRetries: 5 7 | mcp: 8 | servers: 9 | - type: stdio 10 | command: npx 11 | args: 12 | - "-y" 13 | - "@modelcontextprotocol/server-github" 14 | env: 15 | GITHUB_PERSONAL_ACCESS_TOKEN: ${{ env_GITHUB_TOKEN }} 16 | permissions: 17 | maxResponsesPerIssue: 50 -------------------------------------------------------------------------------- /.github/workflows/dewhale-trigger.yaml: -------------------------------------------------------------------------------- 1 | name: Issue Trigger 2 | 3 | on: 4 | issues: 5 | types: [edited, labeled, unlabeled] 6 | issue_comment: 7 | types: [created, edited, deleted] 8 | 9 | jobs: 10 | dewhale-trigger: 11 | runs-on: ubuntu-latest 12 | name: Characters Trigger 13 | if: ${{ !contains(github.event.comment.body, '[Dewhale]') && github.event.comment.user.type != 'bot'}} 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | - name: Setup Deno 18 | uses: denoland/setup-deno@v2 19 | - name: trigger 20 | env: 21 | # auto set by GitHub, details in 22 | # https://docs.github.com/en/actions/security-guides/automatic-token-authentication 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | ACTOR: ${{ github.actor }} 25 | BRANCH: ${{ github.head_ref }} 26 | DEWHALE_CONFIG: ${{ secrets.DEWHALE_CONFIG }} 27 | run: | 28 | echo "${{ secrets.DEWHALE_CONFIG }}" > .env 29 | deno run -A src/entry.ts 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup Deno 20 | uses: denoland/setup-deno@v1 21 | with: 22 | deno-version: '1.x' 23 | 24 | - name: Read jsr.json 25 | id: jsr 26 | run: | 27 | VERSION=$(deno eval 'import { readFileSync } from "node:fs"; const json = JSON.parse(readFileSync("jsr.json", { encoding: "utf8" })); console.log(json.version)') 28 | echo "version=$VERSION" >> $GITHUB_OUTPUT 29 | 30 | - name: Verify tag 31 | run: | 32 | TAG=${GITHUB_REF#refs/tags/} 33 | if [ "$TAG" != "v${{ steps.jsr.outputs.version }}" ]; then 34 | echo "Tag $TAG does not match jsr.json version v${{ steps.jsr.outputs.version }}" 35 | exit 1 36 | fi 37 | 38 | - name: Publish to JSR 39 | run: deno publish 40 | env: 41 | DENO_AUTH_TOKENS: ${{ secrets.JSR_AUTH_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # SvelteKit generated modules 108 | .svelte-kit/ 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | .vscode 136 | 137 | __debug 138 | 139 | # macOS-specific files 140 | .DS_Store 141 | 142 | # temporary files 143 | tmp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

I am rewriting this project, and its v2 version will be a general github-as-a-mcp-host project. If you are interested in its current functionality, please fork the repository and maintain it yourself. Thank you for your attention!

2 | 3 |

4 | 5 |

6 | 7 | # Dewhale 8 | 9 | Dewhale is a GitHub-Powered AI for effortless development. 10 | 11 | [中文文档](./README.zh_CN.md) 12 | 13 | > If you are looking for vx.dev, check our [rebranding discussion](https://github.com/Yuyz0112/dewhale/discussions/189). 14 | 15 | ## Quick Start 16 | 17 | For detailed instructions on how to set up and use Dewhale, please refer to our [Guide](./docs/quick-start.md). 18 | 19 | > With the power of our quota management feature, we allow stargazers to have 3 trials. 20 | 21 | You can also watch this [demo video](http://www.youtube.com/watch?v=J4LAOBRcu2c). 22 | 23 | More examples can be found in the [issue list](https://github.com/Yuyz0112/dewhale/issues?q=is%3Aissue+label%3Aui-gen%2Cvue-ui-gen). 24 | 25 | ## Features 26 | 27 | ### Lower Usage Costs 28 | 29 | Dewhale utilizes prompt engineering techniques under the GPT-4 model to issue commands. The main cost involves the number of input and completion tokens. Our current prompt, found in [prompts/ui-gen.md](./prompts/ui-gen.md), includes instructions for [shadcn/ui](https://ui.shadcn.com/), [lucide](https://lucide.dev/), and [nivo charts](https://nivo.rocks/). 30 | 31 | If you do not need certain components (e.g., charts), you can reduce the API cost per generation by removing instructions from the prompt. 32 | 33 | And you can also switch to other AI models for lower usage costs. 34 | 35 | ### Easy Customization 36 | 37 | Since Dewhale's prompt is open-sourced, you can refer to the existing prompt and replace it with other UI component libraries or coding styles as per your requirements. 38 | 39 | You can also customize the whole workflow by yourself, e.g., a v0.dev-like Web App, and just use Dewhale's prompt as a core. 40 | 41 | ### Seamless GitHub Integration 42 | 43 | The generated code is stored on GitHub, inherently equipped with version control, code review, and collaborative features. 44 | 45 | Additionally, you can use a private repo to keep your code generation results visible only to collaborators. 46 | 47 | ## How It Works 48 | 49 | To understand the underlying architecture and workings of Dewhale, please see our detailed [Architecture Overview](./docs/architecture.md). 50 | 51 | ## Join the Discussions 52 | 53 | If you like the design philosophy of Dewhale, feel free to join our [Github Discussions](https://github.com/Yuyz0112/dewhale/discussions). 54 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Dewhale 6 | 7 | Dewhale 是一个充分发挥 Github 平台能力的通用 AI 工作流。 8 | 9 | ## 快速入门 10 | 11 | 有关如何设置和使用 Dewhale 的详细说明,请参阅我们的[指南](./docs/quick-start.zh_CN.md)。 12 | 13 | > 在配额管理功能的帮助下,我们允许 stargazers 试用 3 次。 14 | 15 | 您也可以观看这个[演示视频](http://www.youtube.com/watch?v=J4LAOBRcu2c)。 16 | 17 | 更多示例可以在[问题列表](https://github.com/Yuyz0112/dewhale/issues?q=is%3Aissue+label%3Aui-gen%2Cvue-ui-gen)中找到。 18 | 19 | ## 特性 20 | 21 | ### 更低的使用成本 22 | 23 | Dewhale 在 GPT-4 模型下利用提示工程技术发出指令。成本主要涉及输入和补全 token 的数量。我们当前的提示词位于 [prompts/ui-gen.md](./prompts/ui-gen.md),包含了如何使用 [shadcn/ui](https://ui.shadcn.com/)、[lucide](https://lucide.dev/) 和 [nivo 图表](https://nivo.rocks/)的指令。 24 | 25 | 如果您不需要某些组件(例如图表),则可以通过从提示中删除指令来减少每次代码生成的 API 成本。 26 | 27 | 您也可以切换到其他 AI 模型以降低使用成本。 28 | 29 | ### 易于定制 30 | 31 | 由于 Dewhale 的提示词是开源的,您可以参考现有的提示并将其替换为其他 UI 组件库或编码风格以满足您的要求。 32 | 33 | 您还可以完全自行定制整个工作流,例如使用 Dewhale 的提示词作为核心,开发一个类似 v0.dev 的 Web 应用。 34 | 35 | ### 无缝 GitHub 集成 36 | 37 | 生成的代码存储在 GitHub 上,天然内置版本控制、代码审查和协作功能。 38 | 39 | 此外,您可以使用私有仓库控制代码的可见性。 40 | 41 | ## 工作原理 42 | 43 | 要了解 Dewhale 的底层架构和工作原理,请参阅我们详细的[架构概述](./docs/architecture.zh_CN.md)。 44 | 45 | ## 加入讨论 46 | 47 | 如果你喜欢 Dewhale 的设计理念,请随时加入我们的 [Github 讨论](https://github.com/Yuyz0112/dewhale/discussions)。 48 | -------------------------------------------------------------------------------- /docs/CD.md: -------------------------------------------------------------------------------- 1 | # Continuously Deploy 2 | 3 | ## Cloudflare Pages 4 | 5 | Follow Cloudflare's [Doc](https://developers.cloudflare.com/pages/get-started/guide/) to connect this repo to Cloudflare Pages. 6 | 7 | ### Configure your build settings 8 | 9 | - Framework preset: `None`` 10 | - Build command: `yarn && yarn build` 11 | - Build output directory: `/dist` 12 | - Root directory: `/preview-ui` 13 | 14 | ### Environment variables 15 | 16 | Add `YARN_ENABLE_IMMUTABLE_INSTALLS=false` to the production and the preview environment variables, which solves an issue of `yarn3`. 17 | -------------------------------------------------------------------------------- /docs/CD.zh_CN.md: -------------------------------------------------------------------------------- 1 | # 持续部署 2 | 3 | ## Cloudflare Pages 4 | 5 | 按照 Cloudflare 的[文档](https://developers.cloudflare.com/pages/get-started/guide/)将此仓库连接到 Cloudflare Pages。 6 | 7 | ### 配置您的构建设置 8 | 9 | - Framework preset: `None`` 10 | - Build command: `yarn && yarn build` 11 | - Build output directory: `/dist` 12 | - Root directory: `/preview-ui` 13 | 14 | ### 环境变量 15 | 16 | 向生产和预览环境变量添加 `YARN_ENABLE_IMMUTABLE_INSTALLS=false`,这可以解决 `yarn3` 的一个问题。 17 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture of Dewhale 2 | 3 | ## Overview 4 | 5 | Dewhale is designed as an open-source alternative to v0.dev, integrating advanced prompt engineering with GitHub for efficient UI generation and code optimization. The architecture of Dewhale comprises three primary components: 6 | 7 | 1. **Prompt Engineering**: Utilizing AI models to output code that meets specific requirements. 8 | 2. **Code Generation Optimization**: Adjusting flawed AI-generated results through manipulation of the code's Abstract Syntax Tree (AST). 9 | 3. **GitHub Integration**: Leveraging GitHub API and GitHub Actions for user interaction and workflow automation. 10 | 11 | ## Prompt 12 | 13 | The prompt forms the core of Dewhale, playing a pivotal role in guiding the AI model to generate high quality code. We have meticulously designed our prompts to closely align with the output style and quality of v0.dev. This involves: 14 | 15 | - **UI Component Library**: We use `@shadcn/ui` as the UI component library, incorporating its usage documentation as examples in our prompts, with slight modifications. 16 | - **Icon Library**: After experiments, we allow AI more freedom in selecting icons from the known `lucide` icon set. Errors in icon imports are corrected in the code generation optimization phase. 17 | - **Chart Library**: Drawing from v0.dev's outputs, we've included usage examples for `@nivo/pie`, `@nivo/line`, `@nivo/heatmap`, `@nivo/scatterplot`, and `@nivo/bar` in our prompts. 18 | 19 | **An important learning from v0.dev is to generate entirely static JSX code, which, despite seemingly limiting use cases, significantly improves stability.** 20 | 21 | ## Code Generation Optimization 22 | 23 | In practice, AI tends to make repetitive mistakes that are hard to eliminate through prompt adjustments. Typical issues include: 24 | 25 | 1. Incorrect import paths for UI components. 26 | 2. Incorrect or missing imports for icons from lucide, or importing non-existent icons. 27 | 28 | After reverse-engineering v0.dev's approach, we've developed a method to correct these issues by analyzing the code's AST. 29 | 30 | We identify the variables used and determine their correct import sources. The lucide project even provides an API to fetch all available icons, allowing us to streamline our prompts and optimize generated code based on the lucide API. 31 | 32 | This optimization significantly enhances the stability of code generation. 33 | 34 | ## GitHub Integration 35 | 36 | The integration with GitHub involves triggering workflows based on issue and issue comment events. The workflow includes: 37 | 38 | - Filtering based on labels, the initiator, and content to decide if code generation should be triggered. 39 | - Creating a branch and corresponding PR for the first-time code generation of an issue. 40 | - Extracting text and images from user input, combined with system prompts, to feed the AI model. The generated code then enters the optimization process where it is refined and improved, followed by the inclusion of the optimized code in the PR. 41 | -------------------------------------------------------------------------------- /docs/architecture.zh_CN.md: -------------------------------------------------------------------------------- 1 | # Dewhale 架构 2 | 3 | > 一篇关于 Dewhale 最初如何逆向工程 v0.dev 实现原理的[博客](https://juejin.cn/post/7316796251149090851)。 4 | 5 | ## 概览 6 | 7 | Dewhale 是一个 v0.dev 的开源替代品,结合提示工程与 GitHub 集成,以实现高效的 UI 生成和代码优化。Dewhale 的架构包含三个主要组件: 8 | 9 | 1. **提示工程**:利用 AI 模型输出满足特定要求的代码。 10 | 2. **代码生成优化**:通过操作代码的抽象语法树(AST)调整有缺陷的 AI 生成结果。 11 | 3. **GitHub 集成**:利用 GitHub API 和 GitHub Actions 进行用户交互和工作流自动化。 12 | 13 | ## 提示 14 | 15 | 提示构成了 Dewhale 的核心,在指导 AI 模型生成高质量代码方面发挥着关键作用。我们精心设计了提示词,以密切匹配 v0.dev 的输出样式和质量。这涉及: 16 | 17 | - **UI 组件库**:我们使用`@shadcn/ui`作为 UI 组件库,将其使用文档作为提示中的示例,并作了轻微修改。 18 | - **图标库**:经过试验,我们允许 AI 在已知的`lucide`图标集中更自由地选择图标。图标导入中的错误将在代码生成优化阶段更正。 19 | - **图表库**:根据 v0.dev 的输出,我们在提示中包含了`@nivo/pie`、`@nivo/line`、`@nivo/heatmap`、`@nivo/scatterplot`和`@nivo/bar`的使用示例。 20 | 21 | **从 v0.dev 获得的一个重要经验是生成完全静态的 JSX 代码,尽管这似乎限制了使用场景,但它显著提高了稳定性。** 22 | 23 | ## 代码生成优化 24 | 25 | 实践中,AI 倾向于重复一些通过调整词也难以消除的错误。遇到的典型问题包括: 26 | 27 | 1. UI 组件的导入路径不正确。 28 | 2. 从 lucide 导入图标时导入不正确或缺少,或导入不存在的图标。 29 | 30 | 在逆向工程 v0.dev 的方法之后,我们开发了一种通过分析代码 AST 来纠正这些问题的方法。 31 | 32 | 我们识别使用的变量及其正确的导入源。lucide 项目甚至提供了一个 API 来获取所有可用的图标,这使我们能够简化提示并基于 lucide API 优化生成的代码。 33 | 34 | 这种优化极大地增强了代码生成的稳定性。 35 | 36 | ## GitHub 集成 37 | 38 | 与 GitHub 的集成涉及基于 issue 和 issue 评论事件触发工作流程。工作流程包括: 39 | 40 | - 根据标签、发起者和内容进行过滤,以决定是否应触发代码生成。 41 | - 为首次代码生成创建分支和相应的 PR。 42 | - 从用户输入中提取文本和图像,与系统提示相结合,为 AI 模型提供数据。生成的代码然后进入优化流程,在那里它被精炼和改进,之后优化后的代码被包含在 PR 中。 43 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | ## Create Repository with Template 4 | 5 | 1. Install **GitHub apps** in your account or organization for labels & badges auto-creating: 6 | 7 | 1. [Probot settings][1] 8 | 2. [PR badge][2] 9 | 10 | 2. Click the **[Use this template][3] button** on the top of this GitHub repository's home page, then create your own repository in the app-installed namespace above. 11 | 12 | ## Setup Repository Secrets 13 | 14 | Users can start by creating their own Dewhale repository based on the current template repo. In the repository's [`settings -> secrets and variables -> actions`][4] section, the following repository secrets need to be set up: 15 | 16 | - `OPENAI_API_KEY`: This is an OpenAI API key. [How to create one?][5] 17 | - `WHITELIST`: A list of GitHub usernames allowed to use Dewhale, separated by commas. Only issues and comments created by users in the WHITELIST will be responded to by Dewhale, ensuring API usage safety. 18 | 19 | ## Create An Issue 20 | 21 | Once set up, users can create issues to describe their requirements. Issues labelled with `ui-gen` will trigger Dewhale to create a PR, including the code generated by Dewhale. 22 | 23 | Users can modify the issue description or iterate on the UI requirements through comments in the issue or PR. All these information will be incorporated into Dewhale's prompt. 24 | 25 | Under the hood, Dewhale use Github Actions to create a workflow, so you can debug by reviewing the workflow logs. 26 | 27 | ## Preview UI 28 | 29 | Dewhale includes a preview UI feature. In the template repository, we continuously deploy via Cloudflare Pages to preview the generated UI results. Alternatively, other service providers can be used for this process. 30 | 31 | [How to configure continuously deploy](./CD.md). 32 | 33 | [1]: https://github.com/apps/settings 34 | [2]: https://pullrequestbadge.com/ 35 | [3]: https://github.com/new?template_name=dewhale&template_owner=Yuyz0112 36 | [4]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository 37 | [5]: https://platform.openai.com/docs/quickstart/account-setup 38 | -------------------------------------------------------------------------------- /docs/quick-start.zh_CN.md: -------------------------------------------------------------------------------- 1 | # 快速入门 2 | 3 | ## 用模板创建仓库 4 | 5 | 1. 为了自动创建专用 label 和 badge,要在你的 GitHub 账号或组织中安装 **GitHub 应用**: 6 | 7 | 1. [Probot settings][1] 8 | 2. [PR badge][2] 9 | 10 | 2. 在本 GitHub 仓库主页顶部点击 **[Use this template][3] 按钮**,然后在安装了上述 GitHub 应用的命名空间中创建你自己的仓库。 11 | 12 | ## 设置仓库 secret 13 | 14 | 用户可以基于当前的模板仓库创建自己的 Dewhale 仓库。在仓库的[settings -> secrets and variables -> actions][4]部分,需要设置以下仓库 secret: 15 | 16 | - `OPENAI_API_KEY`:这是一个 OpenAI API 密钥。[如何创建?][5] 17 | - `WHITELIST`:允许使用 Dewhale 的 GitHub 用户名列表,用逗号分隔。仅由 WHITELIST 中的用户创建的 issue 和评论才会受到 Dewhale 的响应,确保 API 使用安全。 18 | 19 | ## 创建 Issue 20 | 21 | 设置完成后,用户可以创建 issue 来描述他们的需求。标记了 `ui-gen` 的 issue 将触发 Dewhale 生成代码的 PR。 22 | 23 | 用户可以通过 issue 或 PR 中的评论修改 issue 描述或迭代 UI 需求。所有这些信息将被并入 Dewhale 的提示中。 24 | 25 | 在后台,Dewhale 使用 Github Actions 创建工作流,因此您可以通过查看工作流日志进行调试。 26 | 27 | ## 预览 UI 28 | 29 | Dewhale 包含预览 UI 功能。在模板仓库中,我们通过 Cloudflare Pages 不断部署以预览生成的 UI 结果。或者你也可以使用其他部署服务厂商完成这一步骤。 30 | 31 | [如何配置持续部署](./CD.zh_CN.md)。 32 | 33 | [1]: https://github.com/apps/settings 34 | [2]: https://pullrequestbadge.com/ 35 | [3]: https://github.com/new?template_name=dewhale&template_owner=Yuyz0112 36 | [4]: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository 37 | [5]: https://platform.openai.com/docs/quickstart/account-setup 38 | -------------------------------------------------------------------------------- /jsr.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@arcfra/dewhale", 3 | "version": "0.1.16", 4 | "license": "MIT", 5 | "exports": "./src/entry.ts", 6 | "publish": { 7 | "include": ["src/**/*.ts"] 8 | } 9 | } -------------------------------------------------------------------------------- /mcp-servers/shadcn/code-refiner.ts: -------------------------------------------------------------------------------- 1 | import { parse, print, visit, types } from "npm:recast@0.23.11"; 2 | import "npm:@babel/parser@7.27.0"; 3 | import tsParser from "npm:recast@0.23.11/parsers/babel-ts.js"; 4 | 5 | export function refineCode(code: string) { 6 | const fromReact = new Set(); 7 | const usedVariables = new Set(); 8 | const declarations = new Set(); 9 | 10 | const ast = parse(code, { 11 | parser: tsParser, 12 | }); 13 | 14 | visit(ast, { 15 | visitImportDeclaration(p) { 16 | const isReact = 17 | p.node.source.type === "StringLiteral" && 18 | p.node.source.value === "react"; 19 | 20 | if (!isReact) { 21 | p.replace(); 22 | } else { 23 | for (const s of p.node.specifiers || []) { 24 | fromReact.add(s.local?.name.toString() || ""); 25 | } 26 | } 27 | 28 | this.traverse(p); 29 | }, 30 | }); 31 | 32 | visit(ast, { 33 | visitIdentifier(p) { 34 | const varName = p.node.name; 35 | const isDecl = ["VariableDeclarator", "FunctionDeclaration"].includes( 36 | p.parent?.node.type 37 | ); 38 | 39 | if (isDecl) { 40 | declarations.add(varName); 41 | } 42 | 43 | // TODO: collect with a better strategy 44 | // if (!fromReact.has(varName) && !isDecl) { 45 | // usedVariables.add(varName); 46 | // } 47 | this.traverse(p); 48 | }, 49 | visitJSXIdentifier(p) { 50 | const elName = p.node.name; 51 | if ( 52 | p.parent?.node.type === "JSXOpeningElement" && 53 | elName[0].toUpperCase() === elName[0] && 54 | !fromReact.has(elName) 55 | ) { 56 | usedVariables.add(elName); 57 | } 58 | this.traverse(p); 59 | }, 60 | }); 61 | 62 | const { importStr, fallbacks } = mapImports( 63 | Array.from(usedVariables), 64 | declarations 65 | ); 66 | 67 | visit(ast, { 68 | visitJSXIdentifier(p) { 69 | const elName = p.node.name; 70 | if ( 71 | ["JSXOpeningElement", "JSXClosingElement"].includes( 72 | p.parent?.node.type 73 | ) && 74 | elName[0].toUpperCase() === elName[0] && 75 | !fromReact.has(elName) && 76 | fallbacks.includes(elName) 77 | ) { 78 | p.replace(types.builders.jsxIdentifier("div")); 79 | } 80 | this.traverse(p); 81 | }, 82 | }); 83 | 84 | return importStr + print(ast).code; 85 | } 86 | 87 | const shadcnRules = [ 88 | { 89 | matcher: "^Avatar.*", 90 | source: "@/components/ui/avatar", 91 | }, 92 | { 93 | matcher: "^AspectRatio", 94 | source: "@/components/ui/aspect-ratio", 95 | }, 96 | { 97 | matcher: "^Badge", 98 | source: "@/components/ui/badge", 99 | }, 100 | { 101 | matcher: "^Button", 102 | source: "@/components/ui/button", 103 | }, 104 | { 105 | matcher: "^Card.*", 106 | source: "@/components/ui/card", 107 | }, 108 | { 109 | matcher: "^Checkbox", 110 | source: "@/components/ui/checkbox", 111 | }, 112 | { 113 | matcher: "^Collapsible.*", 114 | source: "@/components/ui/collapsible", 115 | }, 116 | { 117 | matcher: "^Menubar.*", 118 | source: "@/components/ui/menubar", 119 | }, 120 | { 121 | matcher: "^Select.*", 122 | source: "@/components/ui/select", 123 | }, 124 | { 125 | matcher: "^RadioGroup.*", 126 | source: "@/components/ui/radio-group", 127 | }, 128 | { 129 | matcher: "^Textarea", 130 | source: "@/components/ui/textarea", 131 | }, 132 | { 133 | matcher: "^ToggleGroup.*", 134 | source: "@/components/ui/toggle-group", 135 | }, 136 | { 137 | matcher: "^Toggle", 138 | source: "@/components/ui/toggle", 139 | }, 140 | { 141 | matcher: "^Skeleton", 142 | source: "@/components/ui/skeleton", 143 | }, 144 | { 145 | matcher: "^Slider", 146 | source: "@/components/ui/slider", 147 | }, 148 | { 149 | matcher: "^Tooltip.*", 150 | source: "@/components/ui/tooltip", 151 | }, 152 | { 153 | matcher: "^Label", 154 | source: "@/components/ui/label", 155 | }, 156 | { 157 | matcher: "^Input", 158 | source: "@/components/ui/input", 159 | }, 160 | { 161 | matcher: "^ScrollArea", 162 | source: "@/components/ui/scroll-area", 163 | }, 164 | { 165 | matcher: "^Switch", 166 | source: "@/components/ui/switch", 167 | }, 168 | { 169 | matcher: "^Dialog.*", 170 | source: "@/components/ui/dialog", 171 | }, 172 | { 173 | matcher: "^Sheet.*", 174 | source: "@/components/ui/sheet", 175 | }, 176 | { 177 | matcher: "^Separator", 178 | source: "@/components/ui/separator", 179 | }, 180 | { 181 | matcher: "^NavigationMenu.*", 182 | source: "@/components/ui/navigation-menu", 183 | }, 184 | { 185 | matcher: "^HoverCard.*", 186 | source: "@/components/ui/hover-card", 187 | }, 188 | { 189 | matcher: "^DropdownMenu.*", 190 | source: "@/components/ui/dropdown-menu", 191 | }, 192 | { 193 | matcher: "^Accordion.*", 194 | source: "@/components/ui/accordion", 195 | }, 196 | { 197 | matcher: "^AlertDialog.*", 198 | source: "@/components/ui/alert-dialog", 199 | }, 200 | { 201 | matcher: "^Alert.*", 202 | source: "@/components/ui/alert", 203 | }, 204 | { 205 | matcher: "^Table.*", 206 | source: "@/components/ui/table", 207 | }, 208 | { 209 | matcher: "^Tabs.*", 210 | source: "@/components/ui/tabs", 211 | }, 212 | { 213 | matcher: "^Popover.*", 214 | source: "@/components/ui/popover", 215 | }, 216 | { 217 | matcher: "^Calendar", 218 | source: "@/components/ui/calendar", 219 | }, 220 | { 221 | matcher: "^Command.*", 222 | source: "@/components/ui/command", 223 | }, 224 | { 225 | matcher: "^ContextMenu.*", 226 | source: "@/components/ui/context-menu", 227 | }, 228 | { 229 | matcher: "^Carousel.*", 230 | source: "@/components/ui/carousel", 231 | }, 232 | { 233 | matcher: "^Drawer.*", 234 | source: "@/components/ui/drawer", 235 | }, 236 | { 237 | matcher: "^Pagination.*", 238 | source: "@/components/ui/pagination", 239 | }, 240 | { 241 | matcher: "^Resizable.*", 242 | source: "@/components/ui/resizable", 243 | }, 244 | { 245 | matcher: "^ResponsiveBar", 246 | source: "@nivo/bar", 247 | }, 248 | { 249 | matcher: "^ResponsiveLine", 250 | source: "@nivo/line", 251 | }, 252 | { 253 | matcher: "^ResponsivePie", 254 | source: "@nivo/pie", 255 | }, 256 | { 257 | matcher: "^ResponsiveScatterPlot", 258 | source: "@nivo/scatterplot", 259 | }, 260 | { 261 | matcher: "^ResponsiveHeatMap", 262 | source: "@nivo/heatmap", 263 | }, 264 | ]; 265 | 266 | const lucideIcons: Record = {}; 267 | try { 268 | const iconNodes = await ( 269 | await fetch("https://lucide.dev/api/icon-nodes") 270 | ).json(); 271 | for (const key in iconNodes) { 272 | const newKey = key 273 | .split("-") 274 | .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) 275 | .join(""); 276 | lucideIcons[newKey] = iconNodes[key]; 277 | } 278 | } catch (error) { 279 | console.error("Error fetching lucide icons:", error); 280 | } 281 | 282 | function mapImports(used: string[], declarations: Set) { 283 | const importMap: Record> = {}; 284 | const fallbacks: string[] = []; 285 | 286 | for (const u of used) { 287 | let source = ""; 288 | let fallback = false; 289 | 290 | for (const rule of shadcnRules) { 291 | if (new RegExp(rule.matcher).test(u)) { 292 | console.log('"shadcn" rule matched:', u, rule); 293 | source = rule.source; 294 | break; 295 | } 296 | } 297 | 298 | if (!source && lucideIcons[u]) { 299 | source = "lucide-react"; 300 | } 301 | 302 | if (!source && declarations.has(u)) { 303 | continue; 304 | } 305 | 306 | if (!source) { 307 | console.log("fallback to Home icon", u); 308 | // fallback to Home icon 309 | source = "lucide-react"; 310 | fallback = true; 311 | fallbacks.push(u); 312 | } 313 | 314 | if (!importMap[source]) { 315 | importMap[source] = new Set(); 316 | } 317 | importMap[source].add(fallback ? "Home" : u); 318 | } 319 | 320 | let importStr = ""; 321 | for (const key in importMap) { 322 | const statement = `import { ${Array.from(importMap[key]).join( 323 | ", " 324 | )} } from '${key}';`; 325 | importStr += `${statement}\r\n`; 326 | } 327 | 328 | return { importStr, fallbacks }; 329 | } 330 | -------------------------------------------------------------------------------- /mcp-servers/shadcn/components.ts: -------------------------------------------------------------------------------- 1 | import { fromMarkdown } from "https://esm.sh/mdast-util-from-markdown@2.0.0"; 2 | import { visitParents } from "https://esm.sh/unist-util-visit-parents@6.0.1"; 3 | import { z } from "npm:zod@3.24.2"; 4 | 5 | const BASE_URL = `https://raw.githubusercontent.com/shadcn-ui/ui/refs/heads/main/apps/www`; 6 | 7 | function kebab(str: string) { 8 | return str 9 | .trim() 10 | .replace(/([a-z])([A-Z])/g, "$1 $2") 11 | .replace(/[\s_]+/g, "-") 12 | .toLowerCase(); 13 | } 14 | 15 | function extractConfig(fileContent: string) { 16 | try { 17 | // Find and extract the configuration object 18 | const configMatch = fileContent.match( 19 | /export const docsConfig: DocsConfig = ({[\s\S]*})/ 20 | ); 21 | 22 | if (!configMatch) { 23 | throw new Error("Could not find docsConfig in the file"); 24 | } 25 | 26 | // Create a safe evaluation context 27 | const configObject = eval(`(${configMatch[1]})`); 28 | return configObject; 29 | } catch (error) { 30 | console.error("Error extracting config:", error); 31 | return null; 32 | } 33 | } 34 | 35 | export async function extractComponents() { 36 | const fileContent = await fetch(`${BASE_URL}/config/docs.ts`).then((res) => 37 | res.text() 38 | ); 39 | 40 | // Extract the configuration 41 | const config = extractConfig(fileContent); 42 | 43 | if (!config) { 44 | return { 45 | components: [], 46 | charts: [], 47 | }; 48 | } 49 | 50 | // Extract components from sidebarNav 51 | const componentsSection = config.sidebarNav.find( 52 | (section: { title: string }) => section.title === "Components" 53 | ); 54 | const components: string[] = componentsSection 55 | ? componentsSection.items.map((item: { title: string }) => 56 | kebab(item.title) 57 | ) 58 | : []; 59 | 60 | // Extract charts from chartsNav 61 | const chartsSection = config.chartsNav.find( 62 | (section: { title: string }) => section.title === "Charts" 63 | ); 64 | const charts: string[] = chartsSection 65 | ? chartsSection.items.map((item: { title: string }) => kebab(item.title)) 66 | : []; 67 | 68 | return { components, charts: [] }; 69 | } 70 | 71 | function extractTsxCodeBlocks(markdownContent: string): string[] { 72 | // Parse the markdown into an AST 73 | const ast = fromMarkdown(markdownContent); 74 | 75 | // Find the heading node for "Usage" 76 | let usageHeadingNode = null; 77 | let usageSectionStart = -1; 78 | let usageSectionEnd = Infinity; 79 | 80 | // Find the Usage heading and its position 81 | visitParents(ast, "heading", (node, ancestors) => { 82 | if ( 83 | node.depth === 2 && 84 | node.children && 85 | node.children[0] && 86 | node.children[0].type === "text" && 87 | node.children[0].value === "Usage" 88 | ) { 89 | usageHeadingNode = node; 90 | usageSectionStart = node.position?.end?.line || -1; 91 | } 92 | }); 93 | 94 | // If no Usage section, return empty array 95 | if (usageSectionStart === -1) { 96 | console.log("No Usage section found in the markdown"); 97 | return []; 98 | } 99 | 100 | // Find the next heading after Usage to determine the end of the section 101 | visitParents(ast, "heading", (node) => { 102 | const headingLine = node.position?.start?.line || Infinity; 103 | if ( 104 | node.depth === 2 && 105 | headingLine > usageSectionStart && 106 | headingLine < usageSectionEnd 107 | ) { 108 | usageSectionEnd = headingLine; 109 | } 110 | }); 111 | 112 | // Extract code blocks with tsx language 113 | const tsxBlocks: string[] = []; 114 | visitParents(ast, "code", (node) => { 115 | const nodeLine = node.position?.start?.line || 0; 116 | 117 | // Check if the code block is within the Usage section and is tsx 118 | if ( 119 | nodeLine > usageSectionStart && 120 | nodeLine < usageSectionEnd && 121 | node.lang === "tsx" 122 | ) { 123 | tsxBlocks.push(node.value); 124 | } 125 | }); 126 | 127 | return tsxBlocks; 128 | } 129 | 130 | export function readFullComponentDoc({ name }: { name: string }) { 131 | return fetch(`${BASE_URL}/content/docs/components/${name}.mdx`).then((res) => 132 | res.text() 133 | ); 134 | } 135 | 136 | export async function readUsageComponentDoc({ name }: { name: string }) { 137 | const fileContent = await readFullComponentDoc({ name }); 138 | 139 | const usageBlocks = extractTsxCodeBlocks(fileContent); 140 | 141 | return `\`\`\`\`tsx 142 | ${usageBlocks.join("\n")} 143 | \`\`\`\``; 144 | } 145 | 146 | export const ComponentSchema = z.object({ 147 | name: z.string(), 148 | necessity: z.enum(["critical", "important", "optional"]), 149 | justification: z.string(), 150 | }); 151 | 152 | export const ComponentsSchema = z.object({ 153 | components: z.array(ComponentSchema), 154 | charts: z.array(ComponentSchema), 155 | }); 156 | 157 | export function createNecessityFilter(necessity: string) { 158 | return (component: { necessity: string }) => { 159 | const score: Record = { 160 | critical: 3, 161 | important: 2, 162 | optional: 1, 163 | }; 164 | return (score[component.necessity] ?? 0) >= (score[necessity] ?? 0); 165 | }; 166 | } 167 | -------------------------------------------------------------------------------- /mcp-servers/shadcn/index.ts: -------------------------------------------------------------------------------- 1 | import "jsr:@std/dotenv@0.225.3/load"; 2 | import { McpServer } from "npm:@modelcontextprotocol/sdk@1.6.1/server/mcp.js"; 3 | import { StdioServerTransport } from "npm:@modelcontextprotocol/sdk@1.6.1/server/stdio.js"; 4 | 5 | import { z } from "npm:zod@3.24.2"; 6 | // import { assert } from "jsr:@std/assert@1.0.11"; 7 | import { 8 | ComponentsSchema, 9 | createNecessityFilter, 10 | extractComponents, 11 | readFullComponentDoc, 12 | readUsageComponentDoc, 13 | } from "./components.ts"; 14 | import { parseMessageToJson } from "./parser.ts"; 15 | import { CREATE_UI, FILTER_COMPONENTS } from "./system-prompts.ts"; 16 | import { refineCode } from "./code-refiner.ts"; 17 | 18 | const server = new McpServer({ 19 | name: "shadcn", 20 | version: "1.0.0", 21 | capabilities: { 22 | resources: {}, 23 | tools: {}, 24 | logging: {}, 25 | }, 26 | }); 27 | 28 | server.tool( 29 | "read-usage-doc", 30 | "read usage doc of a component", 31 | { 32 | name: z.string().describe("name of the component, lowercase, kebab-case"), 33 | }, 34 | async ({ name }) => { 35 | const doc = await readUsageComponentDoc({ name }); 36 | return { 37 | content: [ 38 | { 39 | type: "text", 40 | text: doc, 41 | }, 42 | ], 43 | }; 44 | } 45 | ); 46 | 47 | server.tool( 48 | "read-full-doc", 49 | "read full doc of a component", 50 | { 51 | name: z.string().describe("name of the component, lowercase, kebab-case"), 52 | }, 53 | async ({ name }) => { 54 | const doc = await readFullComponentDoc({ name }); 55 | return { 56 | content: [ 57 | { 58 | type: "text", 59 | text: doc, 60 | }, 61 | ], 62 | }; 63 | } 64 | ); 65 | 66 | server.tool( 67 | "create-ui", 68 | "create Web UI with shadcn/ui components and tailwindcss", 69 | { 70 | description: z.string().describe("description of the Web UI"), 71 | }, 72 | async ({ description }) => { 73 | const components = await extractComponents(); 74 | 75 | const filterComponentsResult = await server.server.createMessage({ 76 | systemPrompt: FILTER_COMPONENTS, 77 | messages: [ 78 | { 79 | role: "user", 80 | content: { 81 | type: "text", 82 | text: `${description}${JSON.stringify( 83 | components 84 | )}`, 85 | }, 86 | }, 87 | ], 88 | maxTokens: 2000, 89 | }); 90 | 91 | const filteredComponents = ComponentsSchema.parse( 92 | parseMessageToJson(filterComponentsResult.content.text as string) 93 | ); 94 | filteredComponents.components.forEach((c) => { 95 | c.name = c.name.toLowerCase(); 96 | }); 97 | filteredComponents.charts.forEach((c) => { 98 | c.name = c.name.toLowerCase(); 99 | }); 100 | 101 | // server.server.sendLoggingMessage({ 102 | // level: "info", 103 | // data: `filter ${components.components.length} components to ${filteredComponents.components.length} components`, 104 | // }); 105 | 106 | const usageDocs = await Promise.all( 107 | filteredComponents.components 108 | .filter(createNecessityFilter("optional")) 109 | .map(async (c) => { 110 | return { 111 | ...c, 112 | doc: await readUsageComponentDoc({ name: c.name }), 113 | }; 114 | }) 115 | ); 116 | 117 | const createUiResult = await server.server.createMessage({ 118 | systemPrompt: CREATE_UI, 119 | messages: [ 120 | { 121 | role: "user", 122 | content: { 123 | type: "text", 124 | text: `${description} 125 | ${usageDocs 126 | .map((d) => { 127 | return ` 128 | ### ${d.name} 129 | 130 | > ${d.justification} 131 | 132 | ${d.doc} 133 | `; 134 | }) 135 | .join("\n")} 136 | `, 137 | }, 138 | }, 139 | ], 140 | maxTokens: 32768, 141 | }); 142 | 143 | const uiCode = createUiResult.content.text as string; 144 | 145 | return { 146 | content: [ 147 | { 148 | type: "text", 149 | text: refineCode(uiCode), 150 | }, 151 | ], 152 | }; 153 | } 154 | ); 155 | 156 | async function main() { 157 | const transport = new StdioServerTransport(); 158 | await server.connect(transport); 159 | console.error("shadcn/UI MCP Server running on stdio"); 160 | } 161 | 162 | main().catch((error) => { 163 | console.error("Fatal error in main():", error); 164 | Deno.exit(1); 165 | }); 166 | -------------------------------------------------------------------------------- /mcp-servers/shadcn/parser.ts: -------------------------------------------------------------------------------- 1 | export function parseMessageToJson(input: string) { 2 | // Regular expression to match JSON code blocks 3 | const jsonCodeBlockRegex = /```json\n([\s\S]*?)\n```/g; 4 | 5 | // Find all matches for JSON code blocks 6 | const matches = Array.from(input.matchAll(jsonCodeBlockRegex)); 7 | 8 | if (matches.length > 1) { 9 | throw new Error("Multiple JSON code blocks found in the input string."); 10 | } 11 | 12 | let jsonString: string; 13 | 14 | if (matches.length === 1) { 15 | // Extract JSON content from the code block, trimming whitespace 16 | jsonString = matches[0][1].trim(); 17 | } else { 18 | // No JSON code block found, use the entire input 19 | jsonString = input.trim(); 20 | } 21 | 22 | try { 23 | // Parse the JSON string into an object 24 | return JSON.parse(jsonString); 25 | } catch (error) { 26 | throw new Error("Failed to parse JSON: " + error + "\n\n" + jsonString); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /mcp-servers/shadcn/system-prompts.ts: -------------------------------------------------------------------------------- 1 | export const FILTER_COMPONENTS = ` 2 | As a web UI expert, analyze the provided UI description thoroughly and identify ONLY the specific components and charts absolutely necessary to implement the described interface. 3 | 4 | Your analysis should: 5 | 1. Consider the exact functional requirements in the description 6 | 2. Identify the minimum set of components needed 7 | 3. Exclude components that might be nice-to-have but aren't essential 8 | 4. Justify each component's selection with a brief reason tied to the requirements 9 | 5. Consider performance and maintainability implications 10 | 11 | I will use your precise component selection to read documentation and implement the UI. 12 | 13 | 14 | { 15 | "components": [ 16 | { 17 | "name": "string", 18 | "necessity": "critical|important|optional", 19 | "justification": "string" 20 | } 21 | ], 22 | "charts": [ 23 | { 24 | "name": "string", 25 | "necessity": "critical|important|optional", 26 | "justification": "string" 27 | } 28 | ] 29 | } 30 | `; 31 | 32 | export const CREATE_UI = ` 33 | You are an expert web developer who specializes in building working website prototypes. Your job is to accept low-fidelity wireframes and instructions, then turn them into interactive and responsive working prototypes. 34 | 35 | 36 | 37 | When sent new designs, you should reply with your best attempt at a high fidelity working prototype as a SINGLE static React JSX file, which export a default component as the UI implementation. 38 | 39 | 40 | 41 | The React component does not accept any props 42 | Everything is hard-coded inside the component 43 | DON'T assume that the component can get any data from outside 44 | All required data should be included in your generated code 45 | Rather than defining data as separate variables, inline it directly in the JSX code 46 | 47 | 48 | 49 | 50 | @/components/ui/$name provided by the available examples 51 | 52 | 53 | 54 | lucide-react 55 | 56 | ArrowRight 57 | Check 58 | Home 59 | User 60 | Search 61 | 62 | 63 | 64 | 65 | 66 | Refer to the usage method in the sample code without omitting any code 67 | Your code should be as complete as possible so users can use it directly 68 | Do not include incomplete content such as "// TODO", "// implement it by yourself", etc. 69 | You can refer to the layout example to beautify the UI layout you generate 70 | 71 | 72 | 73 | Since the code is COMPLETELY STATIC (does not accept any props), there is no need to think too much about scalability and flexibility 74 | It is more important to make its UI results rich and complete 75 | No need to consider the length or complexity of the generated code 76 | 77 | 78 | 79 | Use semantic HTML elements and aria attributes to ensure accessibility 80 | Use Tailwind to adjust spacing, margins and padding between elements, especially when using elements like "main" or "div" 81 | Rely on default styles as much as possible 82 | Avoid adding color to components without explicit instructions 83 | No need to import tailwind.css 84 | 85 | 86 | 87 | 88 | Load from Unsplash 89 | Use solid colored rectangles as placeholders 90 | 91 | 92 | 93 | 94 | Your prototype should look and feel much more complete and advanced than the wireframes provided 95 | Flesh it out, make it real! 96 | Try your best to figure out what the designer wants and make it happen 97 | If there are any questions or underspecified features, use what you know about applications, user experience, and website design patterns to "fill in the blanks" 98 | If you're unsure of how the designs should work, take a guess—it's better to get it wrong than to leave things incomplete 99 | 100 | 101 | 102 | Remember: you love your designers and want them to be happy. The more complete and impressive your prototype, the happier they will be. Good luck, you've got this! 103 | `; 104 | -------------------------------------------------------------------------------- /src/character.ts: -------------------------------------------------------------------------------- 1 | import { join } from "jsr:@std/path@1.0.8"; 2 | import { exists } from "jsr:@std/fs@1.0.14"; 3 | import { 4 | DEFAULT_GLOBAL_CONFIG, 5 | loadGlobalConfig, 6 | merge, 7 | parseYamlWithVariables, 8 | } from "./lib/config.ts"; 9 | import { 10 | CharacterConfig, 11 | DeepPartial, 12 | GlobalConfig, 13 | Issue, 14 | PlatformSdk, 15 | } from "./types.ts"; 16 | import { 17 | generateText, 18 | getModel, 19 | tool, 20 | jsonSchema, 21 | ToolSet, 22 | } from "./lib/llm.ts"; 23 | import { CoreMessage, LanguageModelV1 } from "npm:ai@4.1.54"; 24 | import { McpHub } from "./lib/mcp.ts"; 25 | import { 26 | BRANCH, 27 | OWNER, 28 | REPO, 29 | WORKSPACE, 30 | getPlatformSdk, 31 | } from "./platform/index.ts"; 32 | 33 | export const DEFAULT_CHARACTER_CONFIG: CharacterConfig = { 34 | ...DEFAULT_GLOBAL_CONFIG, 35 | name: "", 36 | labels: [], 37 | systemPrompt: "", 38 | }; 39 | 40 | const CONTEXT = { 41 | WORKSPACE, 42 | REPO, 43 | OWNER, 44 | CURRENT_BRANCH: BRANCH, 45 | }; 46 | 47 | export class Character { 48 | private config: CharacterConfig; 49 | private mcpHub: McpHub; 50 | private sdk: PlatformSdk; 51 | 52 | constructor(config: DeepPartial) { 53 | if (!config.name) { 54 | throw new Error("name is required in character config"); 55 | } 56 | 57 | this.config = merge(DEFAULT_CHARACTER_CONFIG, config); 58 | this.mcpHub = new McpHub({ 59 | servers: this.config.mcp.servers, 60 | }); 61 | this.sdk = getPlatformSdk(); 62 | } 63 | 64 | get name(): string { 65 | return this.config.name; 66 | } 67 | 68 | get model() { 69 | return getModel(this.config.llm.provider, this.config.llm.model); 70 | } 71 | 72 | get unstableModelPreferences() { 73 | return Object.keys( 74 | this.config.llm.__unstable_model_preferences || {} 75 | ).reduce< 76 | Partial< 77 | Record< 78 | keyof Exclude< 79 | GlobalConfig["llm"]["__unstable_model_preferences"], 80 | undefined 81 | >, 82 | LanguageModelV1 83 | > 84 | > 85 | >((prev, cur) => { 86 | const preference = 87 | this.config.llm.__unstable_model_preferences?.[cur as "bestCost"]; 88 | if (preference) { 89 | prev[cur as "bestCost"] = getModel( 90 | preference.provider, 91 | preference.model 92 | ); 93 | } 94 | return prev; 95 | }, {}); 96 | } 97 | 98 | public async initialize() { 99 | await this.mcpHub.connect({ 100 | model: this.model, 101 | unstableModelPreferences: this.unstableModelPreferences, 102 | maxRetries: this.config.llm.maxRetries, 103 | }); 104 | } 105 | 106 | public async finalize() { 107 | await this.mcpHub.disconnect(); 108 | } 109 | 110 | public matchesLabels(issueLabels: Issue["labels"]): boolean { 111 | const issueLabelSet = new Set(issueLabels.map((label) => label.name)); 112 | 113 | return this.config.labels.some((label) => issueLabelSet.has(label)); 114 | } 115 | 116 | private issueToPrompt(issue: Issue): { 117 | messages: CoreMessage[]; 118 | } { 119 | return { 120 | messages: [ 121 | { 122 | role: "system", 123 | content: `${JSON.stringify({ 124 | ...CONTEXT, 125 | ISSUE_ID: issue.id, 126 | CURRENT_TIME: new Date().toISOString(), 127 | })} 128 | ${this.config.systemPrompt}`, 129 | }, 130 | { 131 | role: "user", 132 | content: `${issue.title}${issue.content}`, 133 | }, 134 | ...issue.comments 135 | // .filter((c) => !c.content.includes("[INTERNAL]")) 136 | .map((c) => { 137 | const m: CoreMessage = { 138 | role: "user", 139 | content: c.content, 140 | }; 141 | 142 | return m; 143 | }), 144 | ], 145 | }; 146 | } 147 | 148 | public async doTask(issue: Issue) { 149 | const { messages } = this.issueToPrompt(issue); 150 | const tools = await this.mcpHub.listTools(); 151 | 152 | const { text, steps } = await generateText({ 153 | model: this.model, 154 | messages, 155 | tools: tools.reduce((acc, t) => { 156 | // console.log("appending", t.name, t.description); 157 | acc[t.name] = tool({ 158 | description: t.description, 159 | parameters: jsonSchema(t.inputSchema), 160 | execute: async (input) => { 161 | console.log("going to execute", { name: t.name, input }); 162 | try { 163 | const { content } = await t.client.callTool( 164 | { 165 | name: t.name, 166 | arguments: input as unknown as Record, 167 | }, 168 | undefined, 169 | { 170 | timeout: 600_000, 171 | } 172 | ); 173 | 174 | return JSON.stringify(content); 175 | } catch (error: any) { 176 | console.error(error); 177 | return JSON.stringify({ 178 | error: { 179 | message: error?.message, 180 | name: error?.name, 181 | stack: error?.stack, 182 | ...error, 183 | }, 184 | }); 185 | } 186 | }, 187 | }); 188 | return acc; 189 | }, {} as ToolSet), 190 | maxSteps: this.config.llm.maxSteps, 191 | temperature: this.config.llm.temperature, 192 | maxTokens: this.config.llm.maxTokens, 193 | maxRetries: this.config.llm.maxRetries, 194 | onStepFinish: async (result) => { 195 | console.debug("debug:", result); 196 | const parts: string[] = [result.text].concat( 197 | result.toolCalls 198 | .map((tc) => { 199 | return `[INTERNAL]Tool Call: 200 | \`\`\`json 201 | ${JSON.stringify( 202 | { 203 | toolName: tc.toolName, 204 | args: tc.args, 205 | toolCallId: tc.toolCallId, 206 | }, 207 | null, 208 | 2 209 | )} 210 | \`\`\``; 211 | }) 212 | .concat( 213 | result.toolResults.map((tr) => { 214 | const { toolName, result, toolCallId } = tr as unknown as { 215 | toolName: string; 216 | result: unknown; 217 | toolCallId: string; 218 | }; 219 | return `[INTERNAL]Tool Result: 220 | \`\`\`json 221 | ${JSON.stringify( 222 | { 223 | toolName, 224 | result, 225 | toolCallId, 226 | }, 227 | null, 228 | 2 229 | )} 230 | \`\`\``; 231 | }) 232 | ) 233 | ); 234 | await this.addInternalMessages(issue, parts); 235 | }, 236 | }); 237 | 238 | return { 239 | text, 240 | steps, 241 | }; 242 | } 243 | 244 | private async addInternalMessages(issue: Issue, parts: string[]) { 245 | return await this.sdk.createIssueComment( 246 | issue, 247 | `[Dewhale]\n${parts.filter(Boolean).join("\n\n")}` 248 | ); 249 | } 250 | } 251 | 252 | export async function loadAllCharacters( 253 | basePath: string, 254 | data: Record 255 | ): Promise { 256 | const characters: Character[] = []; 257 | const charactersDir = join(basePath, ".dewhale", "characters"); 258 | const globalConfig = await loadGlobalConfig(basePath, data); 259 | 260 | try { 261 | if (await exists(charactersDir)) { 262 | for await (const entry of Deno.readDir(charactersDir)) { 263 | const isYaml = 264 | entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"); 265 | if (entry.isFile && isYaml) { 266 | try { 267 | const filePath = join(charactersDir, entry.name); 268 | const content = await Deno.readTextFile(filePath); 269 | const config = parseYamlWithVariables( 270 | content, 271 | data 272 | ) as CharacterConfig; 273 | characters.push(new Character(merge(globalConfig, config))); 274 | console.log(`character "${config.name}" loaded`); 275 | } catch (error) { 276 | console.error(`failed to load character "${entry.name}":`, error); 277 | } 278 | } 279 | } 280 | } else { 281 | console.warn(`characters config folder "${charactersDir} not exists"`); 282 | } 283 | } catch (error) { 284 | console.error(`failed to load characters:`, error); 285 | } 286 | 287 | return characters; 288 | } 289 | -------------------------------------------------------------------------------- /src/entry.ts: -------------------------------------------------------------------------------- 1 | import { Character, loadAllCharacters } from "./character.ts"; 2 | import { 3 | getPlatformSdk, 4 | getTriggerEvent, 5 | WORKSPACE, 6 | REPO, 7 | OWNER, 8 | } from "./platform/index.ts"; 9 | import { Issue } from "./types.ts"; 10 | 11 | console.log("hello v2"); 12 | 13 | const data: Record = {}; 14 | for (const [key, value] of Object.entries(Deno.env.toObject())) { 15 | data[`env_${key}`] = value; 16 | } 17 | 18 | const characters = await loadAllCharacters(WORKSPACE, data); 19 | 20 | const event = await getTriggerEvent(); 21 | 22 | const sdk = getPlatformSdk(); 23 | 24 | switch (event?.name) { 25 | case "issues": { 26 | const issue = await sdk.getIssueFromEvent(event); 27 | 28 | await letCharacterDoTask(characters, issue); 29 | break; 30 | } 31 | case "schedule": { 32 | const issues = await sdk.listIssues({ 33 | owner: OWNER, 34 | repo: REPO, 35 | labels: ["schedule"], 36 | }); 37 | for (const issue of issues) { 38 | await letCharacterDoTask(characters, issue); 39 | } 40 | break; 41 | } 42 | default: 43 | console.warn(`Unsupported event`); 44 | } 45 | 46 | async function letCharacterDoTask(characters: Character[], issue: Issue) { 47 | if (issue.state.toLowerCase() !== "open") { 48 | return; 49 | } 50 | 51 | for (const character of characters) { 52 | if (!character.matchesLabels(issue.labels)) { 53 | continue; 54 | } 55 | 56 | // TODO: parallel 57 | await character.initialize(); 58 | 59 | await character.doTask(issue); 60 | 61 | await character.finalize(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | import { join } from "jsr:@std/path@1.0.8"; 2 | import { exists } from "jsr:@std/fs@1.0.14"; 3 | import { parse as parseYaml } from "jsr:@std/yaml@1.0.5"; 4 | import { DeepPartial, GlobalConfig } from "../types.ts"; 5 | 6 | export function merge(target: T, source: DeepPartial): T { 7 | if (typeof source === "object" && source !== null) { 8 | const result = { ...target }; 9 | 10 | for (const key in source) { 11 | if (Object.prototype.hasOwnProperty.call(source, key)) { 12 | const sourceValue = source[key]; 13 | 14 | if (sourceValue === undefined) { 15 | continue; 16 | } 17 | 18 | const targetValue = target[key]; 19 | 20 | if (Array.isArray(sourceValue)) { 21 | result[key] = sourceValue as any; 22 | } else if (typeof sourceValue === "object" && sourceValue !== null) { 23 | if (targetValue && typeof targetValue === "object") { 24 | result[key] = merge(targetValue, sourceValue); 25 | } else { 26 | result[key] = sourceValue as any; 27 | } 28 | } else { 29 | result[key] = sourceValue as any; 30 | } 31 | } 32 | } 33 | 34 | return result; 35 | } 36 | 37 | return source; 38 | } 39 | 40 | export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { 41 | llm: { 42 | provider: "", 43 | model: "", 44 | temperature: 0, 45 | maxTokens: 0, 46 | }, 47 | mcp: { 48 | servers: [], 49 | }, 50 | permissions: { 51 | maxResponsesPerIssue: 0, 52 | }, 53 | }; 54 | 55 | export async function loadGlobalConfig( 56 | basePath: string, 57 | data: Record 58 | ) { 59 | const configPath = join(basePath, ".dewhale", "config.yaml"); 60 | console.log({ configPath }); 61 | 62 | if (!(await exists(configPath))) { 63 | return DEFAULT_GLOBAL_CONFIG; 64 | } 65 | 66 | const configYaml = await Deno.readTextFile(configPath); 67 | const config = parseYamlWithVariables( 68 | configYaml, 69 | data 70 | ) as DeepPartial; 71 | 72 | return merge(DEFAULT_GLOBAL_CONFIG, config); 73 | } 74 | 75 | export function parseYamlWithVariables( 76 | content: string, 77 | data: Record 78 | ) { 79 | const renderedContent = content.replace(/\${{\s*(.*?)\s*}}/g, (_, key) => { 80 | if (!key || !data[key.trim()]) { 81 | throw new Error(`Invalid variable key "${key}"`); 82 | } 83 | return data[key.trim()] || ""; 84 | }); 85 | 86 | return parseYaml(renderedContent); 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/llm.ts: -------------------------------------------------------------------------------- 1 | import "jsr:@std/dotenv@0.225.3/load"; 2 | import { createGoogleGenerativeAI } from "npm:@ai-sdk/google@1.1.20"; 3 | import { createAnthropic } from "npm:@ai-sdk/anthropic@1.1.17"; 4 | import { createOpenAI } from "npm:@ai-sdk/openai@1.2.5"; 5 | import { 6 | generateText, 7 | streamText, 8 | tool, 9 | jsonSchema, 10 | ToolSet, 11 | CoreMessage, 12 | } from "npm:ai@4.1.54"; 13 | import { PromptMessage } from "npm:@modelcontextprotocol/sdk@1.6.1/types.js"; 14 | 15 | const google = createGoogleGenerativeAI({ 16 | baseURL: Deno.env.get("GOOGLE_BASE_URL"), 17 | }); 18 | 19 | const openai = createOpenAI({ 20 | baseURL: Deno.env.get("OPENAI_BASE_URL"), 21 | }); 22 | 23 | const anthropic = createAnthropic({ 24 | baseURL: Deno.env.get("ANTHROPIC_BASE_URL"), 25 | }); 26 | 27 | export function getModel(provider: string, model: string) { 28 | switch (provider) { 29 | case "google": 30 | return google(model); 31 | case "openai": 32 | return openai(model); 33 | case "anthropic": 34 | return anthropic(model); 35 | default: 36 | throw new Error(`Invalid provider "${provider}"`); 37 | } 38 | } 39 | 40 | export function transformMessages(messages: PromptMessage[]): CoreMessage[] { 41 | return messages.map((m) => ({ 42 | role: m.role, 43 | content: [ 44 | { 45 | type: m.content.type as "text", 46 | text: m.content.text as string, 47 | }, 48 | ], 49 | })); 50 | } 51 | 52 | export { generateText, streamText, tool, jsonSchema }; 53 | export type { ToolSet }; 54 | -------------------------------------------------------------------------------- /src/lib/mcp.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "npm:@modelcontextprotocol/sdk@1.6.1/client/index.js"; 2 | import { 3 | getDefaultEnvironment, 4 | StdioClientTransport, 5 | } from "npm:@modelcontextprotocol/sdk@1.6.1/client/stdio.js"; 6 | import { SSEClientTransport } from "npm:@modelcontextprotocol/sdk@1.6.1/client/sse.js"; 7 | import { McpServer } from "../types.ts"; 8 | import { 9 | CreateMessageRequestSchema, 10 | LoggingMessageNotificationSchema, 11 | Tool, 12 | } from "npm:@modelcontextprotocol/sdk@1.6.1/types.js"; 13 | import { LanguageModelV1 } from "npm:ai@4.1.54"; 14 | import { generateText, transformMessages } from "./llm.ts"; 15 | 16 | export interface McpHubOptions { 17 | servers: McpServer[]; 18 | } 19 | 20 | export class McpHub { 21 | private clients: Array<[Client, McpServer]> = []; 22 | private servers: McpServer[] = []; 23 | 24 | constructor({ servers }: McpHubOptions) { 25 | this.servers = servers; 26 | } 27 | 28 | public async connect({ 29 | model: defaultModel, 30 | unstableModelPreferences, 31 | maxRetries, 32 | }: { 33 | model: LanguageModelV1; 34 | unstableModelPreferences: Partial>; 35 | maxRetries?: number; 36 | }) { 37 | for (const server of this.servers) { 38 | const transport = 39 | server.type === "stdio" 40 | ? new StdioClientTransport({ 41 | command: server.command, 42 | args: server.args, 43 | env: { ...getDefaultEnvironment(), ...server.env }, 44 | }) 45 | : server.type === "sse" 46 | ? new SSEClientTransport(new URL(server.url)) 47 | : null; 48 | if (!transport) { 49 | throw new Error(`Unsupported transport type: ${server.type}`); 50 | } 51 | 52 | const client = new Client( 53 | { 54 | name: "dewhale", 55 | version: "1.0.0", 56 | }, 57 | { 58 | capabilities: { 59 | sampling: {}, 60 | }, 61 | } 62 | ); 63 | 64 | client.setNotificationHandler( 65 | LoggingMessageNotificationSchema, 66 | ({ params }) => { 67 | console.log(params.data); 68 | } 69 | ); 70 | 71 | client.setRequestHandler(CreateMessageRequestSchema, async (request) => { 72 | const { 73 | messages, 74 | maxTokens, 75 | systemPrompt, 76 | temperature, 77 | modelPreferences, 78 | } = request.params; 79 | 80 | let model = defaultModel; 81 | const priorities = [ 82 | modelPreferences?.intelligencePriority ?? 0, 83 | modelPreferences?.costPriority ?? 0, 84 | modelPreferences?.speedPriority ?? 0, 85 | ]; 86 | const maxIndex = priorities.indexOf(Math.max(...priorities)); 87 | switch (true) { 88 | case maxIndex === 0 && 89 | unstableModelPreferences.bestIntelligence !== undefined: 90 | model = unstableModelPreferences.bestIntelligence; 91 | break; 92 | case maxIndex === 1 && 93 | unstableModelPreferences.bestCost !== undefined: 94 | model = unstableModelPreferences.bestCost; 95 | break; 96 | case maxIndex === 2 && 97 | unstableModelPreferences.bestSpeed !== undefined: 98 | model = unstableModelPreferences.bestSpeed; 99 | break; 100 | } 101 | 102 | console.log("[INTERNAL]Sampling Request:", { 103 | messages, 104 | systemPrompt, 105 | }); 106 | 107 | // await this.onSampling([ 108 | // `[INTERNAL]Sampling Request: 109 | // \`\`\`json 110 | // ${JSON.stringify( 111 | // { 112 | // messages, 113 | // systemPrompt, 114 | // }, 115 | // null, 116 | // 2 117 | // )} 118 | // \`\`\` 119 | // `, 120 | // ]); 121 | 122 | const fullMessages = transformMessages(messages); 123 | if (systemPrompt) { 124 | fullMessages.unshift({ 125 | role: "system", 126 | content: systemPrompt, 127 | }); 128 | } 129 | 130 | const { text } = await generateText({ 131 | messages: fullMessages, 132 | maxTokens: maxTokens, 133 | model, 134 | temperature, 135 | maxRetries, 136 | }); 137 | 138 | console.log("[INTERNAL]Sampling Result:", text); 139 | 140 | // await this.onSampling([ 141 | // `[INTERNAL]Sampling Result: 142 | // \`\`\` 143 | // ${text} 144 | // \`\`\` 145 | // `, 146 | // ]); 147 | 148 | return { 149 | content: { 150 | type: "text", 151 | text, 152 | }, 153 | model: model.modelId, 154 | role: "assistant", 155 | }; 156 | }); 157 | 158 | await client.connect(transport); 159 | 160 | this.clients.push([client, server]); 161 | } 162 | } 163 | 164 | public async disconnect() { 165 | await Promise.all(this.clients.map(([client]) => client.close())); 166 | } 167 | 168 | public async listTools(): Promise> { 169 | const tools = await Promise.all( 170 | this.clients.map(async ([client, server]) => { 171 | const result = await client.listTools(); 172 | return { 173 | ...result, 174 | client, 175 | server, 176 | }; 177 | }) 178 | ); 179 | 180 | return tools.reduce>( 181 | (acc, { tools, client, server }) => { 182 | return acc.concat( 183 | tools 184 | .filter((t) => { 185 | if (!server.tools) { 186 | // allow all 187 | return true; 188 | } 189 | if (server.tools[t.name]) { 190 | // whitelist 191 | return true; 192 | } 193 | return false; 194 | }) 195 | .map((t) => ({ 196 | ...t, 197 | client, 198 | })) 199 | ); 200 | }, 201 | [] 202 | ); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/platform/github.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "jsr:@std/assert@1.0.11"; 2 | import { join } from "jsr:@std/path@1.0.8"; 3 | import { Octokit } from "npm:octokit@4.1.2"; 4 | import commitPlugin from "npm:octokit-commit-multiple-files@5.0.3"; 5 | import { PlatformSdk, TriggerEvent } from "../types.ts"; 6 | import { IssueEvent, Issue } from "../types.ts"; 7 | 8 | const PatchedOctokit = Octokit.plugin(commitPlugin); 9 | 10 | export class GithubPlatformSdk implements PlatformSdk { 11 | private octokit: Octokit; 12 | 13 | constructor() { 14 | const ghToken = Deno.env.get("GITHUB_TOKEN"); 15 | assert(ghToken, "failed to get github token"); 16 | this.octokit = new PatchedOctokit({ 17 | auth: ghToken, 18 | }); 19 | } 20 | 21 | async getIssueFromEvent(event: IssueEvent) { 22 | const { owner, repo, id } = event.issue; 23 | const { data } = await this.octokit.rest.issues.get({ 24 | owner, 25 | repo, 26 | issue_number: id, 27 | }); 28 | const issueComments = ( 29 | await this.octokit.rest.issues.listComments({ 30 | owner, 31 | repo, 32 | issue_number: id, 33 | }) 34 | ).data; 35 | return { 36 | owner, 37 | repo, 38 | id, 39 | title: data.title, 40 | content: data.body ?? "", 41 | state: data.state, 42 | labels: data.labels.map((l) => { 43 | if (typeof l === "string") { 44 | return { name: l }; 45 | } 46 | return { name: l.name ?? "" }; 47 | }), 48 | comments: issueComments.map((comment) => ({ 49 | author: { 50 | name: comment.user?.login ?? "-", 51 | }, 52 | content: comment.body ?? "", 53 | })), 54 | }; 55 | } 56 | 57 | async listIssues({ 58 | owner, 59 | repo, 60 | labels, 61 | }: { 62 | owner: string; 63 | repo: string; 64 | labels?: string[]; 65 | }) { 66 | const { repository } = await this.octokit.graphql<{ 67 | repository: any; 68 | }>({ 69 | query: /* GraphQL */ ` 70 | query ($owner: String!, $repo: String!, $labels: [String!]) { 71 | repository(owner: $owner, name: $repo) { 72 | issues(labels: $labels, first: 10) { 73 | nodes { 74 | number 75 | title 76 | body 77 | state 78 | labels(first: 10) { 79 | nodes { 80 | name 81 | } 82 | } 83 | comments(first: 10) { 84 | nodes { 85 | author { 86 | login 87 | } 88 | body 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | `, 96 | owner, 97 | repo, 98 | labels, 99 | }); 100 | 101 | return repository.issues.nodes.map((issue: any) => ({ 102 | owner: owner, 103 | repo: repo, 104 | id: issue.number, 105 | title: issue.title, 106 | content: issue.body, 107 | state: issue.state, 108 | labels: issue.labels.nodes.map((l: any) => ({ 109 | name: l.name, 110 | })), 111 | comments: issue.comments.nodes.map((comment: any) => ({ 112 | author: { 113 | name: comment.author.login, 114 | }, 115 | content: comment.body, 116 | })), 117 | })); 118 | } 119 | 120 | async createIssueComment(issue: Issue, content: string) { 121 | await this.octokit.rest.issues.createComment({ 122 | owner: issue.owner, 123 | repo: issue.repo, 124 | issue_number: issue.id, 125 | body: content, 126 | }); 127 | } 128 | } 129 | 130 | const __dirname = new URL(".", import.meta.url).pathname; 131 | 132 | export function getGithubContext() { 133 | let WORKSPACE = join(__dirname, "../../"); 134 | if (Deno.env.get("GITHUB_WORKSPACE")) { 135 | WORKSPACE = Deno.env.get("GITHUB_WORKSPACE")!; 136 | } 137 | assert(WORKSPACE, "WORKSPACE is not set"); 138 | 139 | let REPO = ""; 140 | let OWNER = ""; 141 | if (Deno.env.get("GITHUB_REPOSITORY")) { 142 | const [owner, repo] = Deno.env.get("GITHUB_REPOSITORY")!.split("/"); 143 | REPO = repo; 144 | OWNER = owner; 145 | } 146 | 147 | let BRANCH = ""; 148 | if (Deno.env.get("BRANCH")) { 149 | BRANCH = Deno.env.get("BRANCH")!; 150 | } 151 | 152 | return { 153 | WORKSPACE, 154 | REPO, 155 | OWNER, 156 | BRANCH, 157 | }; 158 | } 159 | 160 | export async function getGithubTriggerEvent(): Promise { 161 | let eventName = ""; 162 | 163 | if (Deno.env.get("GITHUB_EVENT_NAME")) { 164 | eventName = Deno.env.get("GITHUB_EVENT_NAME")!; 165 | } 166 | 167 | assert(eventName, "failed to get event name"); 168 | 169 | // deno-lint-ignore no-explicit-any 170 | let eventPayload: any = {}; 171 | 172 | if (Deno.env.get("GITHUB_EVENT_PATH")) { 173 | eventPayload = JSON.parse( 174 | await Deno.readTextFile(Deno.env.get("GITHUB_EVENT_PATH")!) 175 | ); 176 | } 177 | 178 | if (eventName === "issues" || eventName === "issue_comment") { 179 | return { 180 | name: "issues", 181 | issue: { 182 | owner: eventPayload.repository.owner.login, 183 | repo: eventPayload.repository.name, 184 | id: eventPayload.issue.number, 185 | }, 186 | }; 187 | } 188 | 189 | if (eventName === "schedule") { 190 | return { 191 | name: "schedule", 192 | }; 193 | } 194 | 195 | console.warn(`Unsupported event: ${eventName}`); 196 | 197 | return null; 198 | } 199 | -------------------------------------------------------------------------------- /src/platform/gitlab.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "jsr:@std/assert@1.0.11"; 2 | import { join } from "jsr:@std/path@1.0.8"; 3 | import { Gitlab } from "npm:@gitbeaker/rest@42.2.0"; 4 | import * as core from "npm:@gitbeaker/core@42.2.0"; 5 | import { Issue, PlatformSdk, TriggerEvent } from "../types.ts"; 6 | import { IssueEvent } from "../types.ts"; 7 | 8 | function getLabelName(l: core.IssueSchema["labels"][0]) { 9 | if (typeof l === "string") { 10 | return l; 11 | } 12 | return l.name; 13 | } 14 | 15 | export class GitlabPlatformSdk implements PlatformSdk { 16 | private gitlab: core.Gitlab; 17 | 18 | constructor() { 19 | const glToken = Deno.env.get("GITLAB_TOKEN"); 20 | if (!glToken) { 21 | throw new Error("failed to get gitlab token"); 22 | } 23 | const glHost = Deno.env.get("GITLAB_HOST") ?? "https://gitlab.com"; 24 | 25 | this.gitlab = new Gitlab({ 26 | token: glToken, 27 | host: glHost, 28 | }); 29 | } 30 | 31 | async getIssueFromEvent(event: IssueEvent) { 32 | const { repo: projectId, id } = event.issue; 33 | const issue = await this.gitlab.Issues.show(id, { 34 | projectId, 35 | }); 36 | 37 | const project = await this.gitlab.Projects.show(projectId); 38 | 39 | const state = issue.state === "opened" ? "open" : issue.state; 40 | const issueComments = await this.gitlab.IssueNotes.all(projectId, id, { 41 | sort: "asc", 42 | orderBy: "created_at", 43 | }); 44 | 45 | return { 46 | owner: project.namespace.name, 47 | repo: projectId, 48 | id, 49 | title: issue.title, 50 | content: issue.description ?? "", 51 | state, 52 | labels: issue.labels.map((l) => ({ name: getLabelName(l) })), 53 | comments: issueComments 54 | .filter((c) => !c.system) 55 | .map((comment) => ({ 56 | author: { 57 | name: comment.author.name ?? "-", 58 | }, 59 | content: comment.body ?? "", 60 | })), 61 | }; 62 | } 63 | 64 | async listIssues(options: { 65 | owner: string; 66 | repo: string; 67 | labels?: string[]; 68 | }): Promise { 69 | const { repo: projectId, labels } = options; 70 | const issueList = await this.gitlab.Issues.all({ 71 | projectId, 72 | labels: labels?.join(","), 73 | }); 74 | 75 | const project = await this.gitlab.Projects.show(projectId); 76 | 77 | const issues: Issue[] = []; 78 | 79 | for (const issue of issueList) { 80 | const state = issue.state === "opened" ? "open" : issue.state; 81 | const issueComments = await this.gitlab.IssueNotes.all( 82 | projectId, 83 | issue.iid, 84 | { 85 | sort: "asc", 86 | orderBy: "created_at", 87 | } 88 | ); 89 | 90 | issues.push({ 91 | owner: project.namespace.name, 92 | repo: projectId, 93 | id: issue.iid, 94 | title: issue.title, 95 | content: issue.description ?? "", 96 | state, 97 | labels: issue.labels.map((l) => ({ name: l })), 98 | comments: issueComments 99 | .filter((c) => !c.system) 100 | .map((comment) => ({ 101 | author: { 102 | name: comment.author.name ?? "-", 103 | }, 104 | content: comment.body ?? "", 105 | })), 106 | }); 107 | } 108 | 109 | return issues; 110 | } 111 | 112 | async createIssueComment(issue: Issue, content: string): Promise { 113 | await this.gitlab.IssueNotes.create(issue.repo, issue.id, content); 114 | } 115 | } 116 | 117 | const __dirname = new URL(".", import.meta.url).pathname; 118 | 119 | export function getGitLabContext() { 120 | let WORKSPACE = join(__dirname, "../../"); 121 | if (Deno.env.get("CI_PROJECT_DIR")) { 122 | WORKSPACE = Deno.env.get("CI_PROJECT_DIR")!; 123 | } 124 | assert(WORKSPACE, "WORKSPACE is not set"); 125 | 126 | let REPO = ""; 127 | let OWNER = ""; 128 | if (Deno.env.get("CI_PROJECT_NAMESPACE") && Deno.env.get("CI_PROJECT_ID")) { 129 | OWNER = Deno.env.get("CI_PROJECT_NAMESPACE")!; 130 | REPO = Deno.env.get("CI_PROJECT_ID")!; 131 | } 132 | 133 | let BRANCH = ""; 134 | if (Deno.env.get("CI_COMMIT_REF_NAME")) { 135 | BRANCH = Deno.env.get("CI_COMMIT_REF_NAME")!; 136 | } 137 | 138 | return { 139 | WORKSPACE, 140 | REPO, 141 | OWNER, 142 | BRANCH, 143 | }; 144 | } 145 | 146 | export async function getGitlabTriggerEvent(): Promise { 147 | // deno-lint-ignore no-explicit-any 148 | let eventPayload: 149 | | core.WebhookIssueEventSchema 150 | | core.WebhookIssueNoteEventSchema 151 | | null = null; 152 | 153 | if (Deno.env.get("CI_EVENT_PATH")) { 154 | eventPayload = JSON.parse( 155 | await Deno.readTextFile(Deno.env.get("CI_EVENT_PATH")!) 156 | ); 157 | } 158 | 159 | if ( 160 | eventPayload?.event_type === "issue" || 161 | eventPayload?.event_type === "note" 162 | ) { 163 | return { 164 | name: "issues", 165 | issue: { 166 | owner: eventPayload.project.namespace, 167 | repo: eventPayload.project.id.toString(), 168 | id: 169 | eventPayload.event_type === "issue" 170 | ? eventPayload.object_attributes.iid 171 | : eventPayload.issue.iid, 172 | }, 173 | }; 174 | } 175 | 176 | return null; 177 | } 178 | -------------------------------------------------------------------------------- /src/platform/index.ts: -------------------------------------------------------------------------------- 1 | import { PlatformSdk, PlatformType, TriggerEvent } from "../types.ts"; 2 | import { 3 | GithubPlatformSdk, 4 | getGithubContext, 5 | getGithubTriggerEvent, 6 | } from "./github.ts"; 7 | import { 8 | getGitLabContext, 9 | getGitlabTriggerEvent, 10 | GitlabPlatformSdk, 11 | } from "./gitlab.ts"; 12 | 13 | const PLATFORM_TYPE = (Deno.env.get("PLATFORM_TYPE") ?? 14 | "github") as PlatformType; 15 | 16 | const { WORKSPACE, BRANCH, OWNER, REPO } = 17 | PLATFORM_TYPE === "github" 18 | ? getGithubContext() 19 | : PLATFORM_TYPE === "gitlab" 20 | ? getGitLabContext() 21 | : { 22 | WORKSPACE: "", 23 | BRANCH: "", 24 | OWNER: "", 25 | REPO: "", 26 | }; 27 | 28 | export { WORKSPACE, BRANCH, OWNER, REPO }; 29 | 30 | export function getTriggerEvent(): Promise { 31 | switch (PLATFORM_TYPE) { 32 | case "github": 33 | return getGithubTriggerEvent(); 34 | case "gitlab": 35 | return getGitlabTriggerEvent(); 36 | default: 37 | throw new Error(`Invalid platform type "${PLATFORM_TYPE}"`); 38 | } 39 | } 40 | 41 | export function getPlatformSdk(): PlatformSdk { 42 | switch (PLATFORM_TYPE) { 43 | case "github": 44 | return new GithubPlatformSdk(); 45 | case "gitlab": 46 | return new GitlabPlatformSdk(); 47 | default: 48 | throw new Error(`Invalid platform type "${PLATFORM_TYPE}"`); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface GlobalConfig { 2 | llm: { 3 | provider: string; 4 | model: string; 5 | temperature?: number; 6 | maxTokens?: number; 7 | maxRetries?: number; 8 | maxSteps?: number; 9 | __unstable_model_preferences?: { 10 | bestIntelligence?: { 11 | provider: string; 12 | model: string; 13 | }; 14 | bestCost?: { 15 | provider: string; 16 | model: string; 17 | }; 18 | bestSpeed?: { 19 | provider: string; 20 | model: string; 21 | }; 22 | }; 23 | }; 24 | mcp: { 25 | servers: McpServer[]; 26 | }; 27 | permissions: Permissions; 28 | } 29 | 30 | export interface CharacterConfig extends GlobalConfig { 31 | name: string; 32 | labels: string[]; 33 | systemPrompt: string; 34 | } 35 | 36 | export type McpServer = { 37 | env?: Record; 38 | tools?: Record; 39 | } & McpServerTransport; 40 | 41 | export type McpServerTransport = 42 | | { 43 | type: "stdio"; 44 | command: string; 45 | args?: string[]; 46 | } 47 | | { 48 | type: "sse"; 49 | url: string; 50 | }; 51 | 52 | export interface Permissions { 53 | maxResponsesPerIssue: number; 54 | } 55 | 56 | export type DeepPartial = { 57 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; 58 | }; 59 | 60 | export type TriggerEvent = IssueEvent | ScheduleEvent; 61 | 62 | export type IssueEvent = { 63 | name: "issues"; 64 | issue: { 65 | owner: string; // github.owner, gitlab.namespace 66 | repo: string; // github.repo, gitlab.project_id 67 | id: number; // github.issue.id, gitlab.issue.iid 68 | }; 69 | }; 70 | 71 | export type ScheduleEvent = { 72 | name: "schedule"; 73 | }; 74 | 75 | export type PlatformType = "github" | "gitlab"; 76 | 77 | export interface PlatformSdk { 78 | getIssueFromEvent(event: IssueEvent): Promise; 79 | listIssues(options: { 80 | owner: string; 81 | repo: string; 82 | labels?: string[]; 83 | }): Promise; 84 | createIssueComment(issue: Issue, content: string): Promise; 85 | } 86 | 87 | export interface Issue { 88 | owner: string; // github.owner, gitlab.namespace 89 | repo: string; // github.repo, gitlab.project_id 90 | id: number; // github.issue.id, gitlab.issue.iid 91 | title: string; 92 | content: string; 93 | state: string; // github's 'open' state and gitlab's 'opened' state are normalized to 'open' 94 | labels: { name: string }[]; 95 | comments: IssueComment[]; 96 | } 97 | 98 | export interface IssueComment { 99 | author: { 100 | name: string; 101 | }; 102 | content: string; 103 | } 104 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Basics 2 | 3 | ```sh 4 | npm create astro@latest -- --template basics 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554) 14 | 15 | ## 🚀 Project Structure 16 | 17 | Inside of your Astro project, you'll see the following folders and files: 18 | 19 | ```text 20 | / 21 | ├── public/ 22 | │ └── favicon.svg 23 | ├── src/ 24 | │ ├── components/ 25 | │ │ └── Card.astro 26 | │ ├── layouts/ 27 | │ │ └── Layout.astro 28 | │ └── pages/ 29 | │ └── index.astro 30 | └── package.json 31 | ``` 32 | 33 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 34 | 35 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 36 | 37 | Any static assets, like images, can be placed in the `public/` directory. 38 | 39 | ## 🧞 Commands 40 | 41 | All commands are run from the root of the project, from a terminal: 42 | 43 | | Command | Action | 44 | | :------------------------ | :----------------------------------------------- | 45 | | `npm install` | Installs dependencies | 46 | | `npm run dev` | Starts local dev server at `localhost:4321` | 47 | | `npm run build` | Build your production site to `./dist/` | 48 | | `npm run preview` | Preview your build locally, before deploying | 49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 50 | | `npm run astro -- --help` | Get help using the Astro CLI | 51 | 52 | ## 👀 Want to learn more? 53 | 54 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 55 | -------------------------------------------------------------------------------- /website/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | 3 | // https://astro.build/config 4 | export default defineConfig({}); 5 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "astro": "^4.2.6", 14 | "@astrojs/check": "^0.4.1", 15 | "typescript": "^5.3.3" 16 | } 17 | } -------------------------------------------------------------------------------- /website/public/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuyz0112/dewhale/2e11c0cd6ed3de2e5029a6d5fee320fefc8f3d51/website/public/bg.png -------------------------------------------------------------------------------- /website/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuyz0112/dewhale/2e11c0cd6ed3de2e5029a6d5fee320fefc8f3d51/website/public/logo.png -------------------------------------------------------------------------------- /website/src/components/ImgAndText.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | img: string; 4 | align: "left" | "right"; 5 | } 6 | 7 | const { img, align } = Astro.props; 8 | --- 9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 | 17 | 63 | -------------------------------------------------------------------------------- /website/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /website/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 19 | 20 | {title} 21 | 24 | 25 | 26 | 27 | 28 | 29 | 48 | -------------------------------------------------------------------------------- /website/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from "../layouts/Layout.astro"; 3 | import ImgAndText from "../components/ImgAndText.astro"; 4 | --- 5 | 6 | 7 |
8 |
9 |
10 |
11 | 12 |

Dewhale

13 |

Dewhale is a GitHub-Powered AI for effortless development.

14 |
15 |
16 | Quick Start 21 | Source Code 26 |
27 |
28 |
29 |

Start the Journey

30 | 34 |
35 | As a user, describe your needs in the way you are most used to - 36 | submit a Github issue. 37 |
38 |
39 | 40 | 44 |
45 | Choose a code generation mode through the issue template. 48 |
49 | Dewhale currently supports generating UI code based on React, Vue, and 50 | Svelte. You can also add your own custom code generation modes at any 51 | time. 52 |
53 |
54 | 55 | 59 |
60 | Describe your needs in the Issue, both images and text are 61 | acceptable. 62 |
63 | In Dewhale, we have designed a strict permission mechanism. 64 |
65 | By default, only Issues submitted by whitelisted users will trigger generation. 66 | However, we also provide flexible and powerful quota management capabilities. 67 |
68 |
69 | 70 | 74 |
75 | After submitting the issue, the code generation Github Action will be triggered. 78 |
79 |
80 | 81 | 85 |
86 | In moments, it runs successfully through a transparent process, 87 | leveraging workflows users know to facilitate observability and 88 | troubleshooting. 89 |
90 |
91 | 92 | 96 |
97 | The generated code is presented in the most intuitive and familiar 98 | way: a pull request. 99 |
100 | Integrated with your favorite code deployment platform for instant previews. 101 |
102 |
103 | 104 | 108 |
109 | In the preview UI, you can visually review the generated results and 110 | copy the code. 111 |
112 |
113 | 114 | 118 |
119 | When you want to modify requirements, you can continue the 120 | conversation in the pull request. 121 |
122 |
123 | 124 | 128 |
129 | According to your latest instructions, the visual style of the UI 130 | generated again and remains consistent. 131 |
132 |
133 | 134 | 138 |
139 | Want to direct the AI to modify the code more precisely? 140 |
141 | Use the most intuitive method you can imagine: code review. 144 |
145 |
146 | 147 | 151 |
It works like a charm.
152 |
153 | 154 | 158 |
159 | Dewhale is GitHub-powered, so you obviously don't need to worry 160 | about version control. 161 |
162 |
163 | 164 | 168 |
169 | "Self-deployment" is even easier, just fork the 170 | repository and configure your openAI API key and Github Token 171 | according to the guide. 172 |
173 |
174 | 175 | 179 |
180 | Based on flexible and powerful quota management capabilities, you 181 | can set usage limits for different users and groups. 182 |
183 | Instead of developing your own trial and subscription systems, why not 184 | use Github Stargazers and Sponsors? 185 |
186 |
187 |
188 | 189 |
190 |

Happy Hacking

191 | 192 |
193 |
194 |

195 | If you like the design philosophy of Dewhale, feel free to follow: 196 |

197 |
198 |
199 | Github Discussions 204 | X.com 209 |
210 |
211 |
212 |
213 |
214 |
215 | 216 | 340 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } --------------------------------------------------------------------------------