├── .eslintignore ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── site.yml ├── .gitignore ├── .prettierrc ├── .spellbound ├── description.md └── pinecone_cache.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── ARCHITECTURE.md ├── CHANGELOG.md ├── LICENSE.md ├── LOCAL_SETUP.md ├── README.md ├── apps ├── agentitive-site │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── blog │ │ ├── 2023-04-05-new-site │ │ │ ├── docusaurus-plushie-banner.jpeg │ │ │ └── index.md │ │ └── authors.yml │ ├── docs │ │ ├── intro.md │ │ └── local_dev.md │ ├── docusaurus.config.js │ ├── package-lock.json │ ├── package.json │ ├── sidebars.js │ ├── src │ │ ├── components │ │ │ └── HomepageFeatures │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ ├── css │ │ │ └── custom.css │ │ ├── definitions.d.ts │ │ └── pages │ │ │ ├── index.module.css │ │ │ ├── index.tsx │ │ │ └── markdown-page.md │ ├── static │ │ ├── .nojekyll │ │ └── img │ │ │ ├── favicon.ico │ │ │ └── logo.svg │ └── tsconfig.json ├── spellbound-shared │ ├── package.json │ ├── src │ │ ├── birpc │ │ │ ├── birpc.ts │ │ │ ├── extension.ts │ │ │ ├── index.ts │ │ │ └── webview.ts │ │ ├── index.ts │ │ └── types │ │ │ ├── Message.ts │ │ │ └── index.ts │ └── tsconfig.json ├── spellbound-ui │ ├── .gitignore │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── scripts │ │ └── build-react-no-split.js │ ├── src │ │ ├── backup.ts │ │ ├── components │ │ │ ├── App │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.scss │ │ │ ├── InputPanel │ │ │ │ ├── InputPanel.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.module.scss │ │ │ ├── MessageHistory │ │ │ │ ├── MessageHistory.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.module.scss │ │ │ ├── MessageItem │ │ │ │ ├── MessageItem.tsx │ │ │ │ ├── index.ts │ │ │ │ └── styles.module.scss │ │ │ ├── index.ts │ │ │ └── styles.ts │ │ ├── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── store.ts │ │ ├── styles.scss │ │ └── utilities │ │ │ ├── rpc.ts │ │ │ └── vscode.ts │ ├── tsconfig.json │ └── webpack.config.js └── spellbound │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .gitattributes │ ├── .gitignore │ ├── .spellbound │ ├── description.md │ └── pinecone_cache.json │ ├── .vscode │ └── launch.json │ ├── assets │ ├── icon.png │ ├── prompt.md │ └── wand-magic-sparkles-solid.svg │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── agent │ │ ├── AgentLogicHandler.ts │ │ └── index.ts │ ├── api │ │ ├── openai.ts │ │ └── pinecone.ts │ ├── commands │ │ ├── indexFolder.ts │ │ └── indexWorkspace.ts │ ├── environment │ │ ├── getCurrentModel.ts │ │ ├── getModelList.ts │ │ ├── getOpenAIKey.ts │ │ ├── getPineconeIndex.ts │ │ └── getPineconeKey.ts │ ├── extension.ts │ ├── inference │ │ └── InferenceHandler.ts │ ├── prompts │ │ └── fillPrompt.ts │ ├── tools │ │ ├── AnyToolInterface.ts │ │ ├── ToolEngine.ts │ │ ├── ask │ │ │ ├── AskToolInterface.ts │ │ │ └── askToolImpl.ts │ │ ├── cat │ │ │ ├── CatToolInterface.ts │ │ │ └── catToolImpl.ts │ │ ├── diff │ │ │ ├── DiffToolInterface.ts │ │ │ └── diffToolImpl.ts │ │ ├── done │ │ │ ├── DoneToolInterface.ts │ │ │ └── doneToolImpl.ts │ │ ├── git │ │ │ ├── GitToolInterface.ts │ │ │ ├── gitToolImpl.ts │ │ │ └── utility │ │ │ │ └── runGitCommand.ts │ │ ├── grep │ │ │ ├── GrepToolInterface.ts │ │ │ └── grepToolImpl.ts │ │ ├── ls │ │ │ ├── LsToolInterface.ts │ │ │ └── lsToolImpl.ts │ │ ├── move │ │ │ ├── MoveToolInterface.ts │ │ │ └── moveToolImpl.ts │ │ ├── npm │ │ │ ├── NpmToolInterface.ts │ │ │ ├── npmToolImpl.ts │ │ │ └── utility │ │ │ │ └── runNpmCommand.ts │ │ ├── search │ │ │ ├── SearchToolInterface.ts │ │ │ └── searchToolImpl.ts │ │ ├── stats │ │ │ ├── StatsToolInterface.ts │ │ │ └── statsToolImpl.ts │ │ └── write │ │ │ ├── WriteToolInterface.ts │ │ │ └── writeToolImpl.ts │ ├── utils │ │ ├── chunking.ts │ │ ├── getCurrentWorkspaceFolder.ts │ │ ├── paths.ts │ │ └── uuid.ts │ └── views │ │ └── ChatboxViewProvider.ts │ ├── tsconfig.json │ └── webpack.config.js ├── assets └── logo.png ├── common ├── config │ └── rush │ │ ├── .npmrc │ │ ├── .npmrc-publish │ │ ├── .pnpmfile.cjs │ │ ├── artifactory.json │ │ ├── build-cache.json │ │ ├── command-line.json │ │ ├── common-versions.json │ │ ├── experiments.json │ │ ├── pnpm-config.json │ │ ├── pnpm-lock.yaml │ │ ├── repo-state.json │ │ ├── rush-plugins.json │ │ └── version-policies.json ├── git-hooks │ └── commit-msg.sample └── scripts │ ├── install-run-rush-pnpm.js │ ├── install-run-rush.js │ ├── install-run-rushx.js │ └── install-run.js ├── rigs ├── eslint-plugin-local │ ├── README.md │ ├── lib │ │ └── index.js │ └── package.json ├── node-rig │ ├── package.json │ └── profiles │ │ └── default │ │ └── tsconfig-base.json └── repo-scripts │ ├── package.json │ ├── src │ ├── audit.ts │ ├── bumpVersions.ts │ ├── generateSnippets.ts │ ├── publishLibrary.ts │ ├── publishLibs.ts │ ├── utils │ │ ├── index.ts │ │ ├── npm │ │ │ ├── PackageMetadata.ts │ │ │ ├── getRemotePackageDependencies.ts │ │ │ ├── getRemotePackageMetadata.ts │ │ │ ├── getRemotePackageVersion.ts │ │ │ └── index.ts │ │ └── reflect │ │ │ ├── allApps.ts │ │ │ ├── allLibraries.ts │ │ │ ├── allProjects.ts │ │ │ └── index.ts │ └── versions.ts │ ├── templates │ └── tsconfig.json │ └── tsconfig.json └── rush.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't allow people to merge changes to these generated files, because the result 2 | # may be invalid. You need to run "rush update" again. 3 | pnpm-lock.yaml merge=text 4 | shrinkwrap.yaml merge=binary 5 | npm-shrinkwrap.json merge=binary 6 | yarn.lock merge=binary 7 | 8 | # Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic 9 | # syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor 10 | # may also require a special configuration to allow comments in JSON. 11 | # 12 | # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 13 | # 14 | *.json linguist-language=JSON-with-Comments 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ "main" ] 5 | pull_request: 6 | branches: [ "main" ] 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 2 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 16 17 | - name: Verify Change Logs 18 | run: node common/scripts/install-run-rush.js change --verify 19 | - name: Rush Install 20 | run: node common/scripts/install-run-rush.js install 21 | - name: Rush rebuild 22 | run: node common/scripts/install-run-rush.js rebuild --verbose 23 | -------------------------------------------------------------------------------- /.github/workflows/site.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | permissions: write-all 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | # Review gh actions docs if you want to further define triggers, paths, etc 10 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on 11 | 12 | jobs: 13 | deploy: 14 | name: Deploy to GitHub Pages 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | 22 | - name: Rush Install 23 | run: node common/scripts/install-run-rush.js install 24 | - name: Build website 25 | run: node common/scripts/install-run-rush.js build --to agentitive-site 26 | 27 | # Popular action to deploy to GitHub Pages: 28 | # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus 29 | - name: Deploy to GitHub Pages 30 | uses: peaceiris/actions-gh-pages@v3 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | # Build output to publish to the `gh-pages` branch: 34 | publish_dir: ./apps/agentitive-site/build 35 | # The following lines assign commit authorship to the official 36 | # GH-Actions bot for deploys to `gh-pages` branch: 37 | # https://github.com/actions/checkout/issues/13#issuecomment-724415212 38 | # The GH actions bot is used by default if you didn't specify the two fields. 39 | # You can swap them out with your own user credentials. 40 | user_name: github-actions[bot] 41 | user_email: 41898282+github-actions[bot]@users.noreply.github.com -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # Bower dependency directory (https://bower.io/) 25 | bower_components 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (https://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules/ 35 | jspm_packages/ 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional eslint cache 41 | .eslintcache 42 | 43 | # Optional REPL history 44 | .node_repl_history 45 | 46 | # Output of 'npm pack' 47 | *.tgz 48 | 49 | # Yarn Integrity file 50 | .yarn-integrity 51 | 52 | # dotenv environment variables file 53 | .env 54 | 55 | # next.js build output 56 | .next 57 | 58 | # OS X temporary files 59 | .DS_Store 60 | 61 | # IntelliJ IDEA project files; if you want to commit IntelliJ settings, this recipe may be helpful: 62 | # https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 63 | .idea/ 64 | *.iml 65 | 66 | # Rush temporary files 67 | common/deploy/ 68 | common/temp/ 69 | common/autoinstallers/*/.npmrc 70 | **/.rush/temp/ 71 | 72 | # Heft temporary files 73 | .heft 74 | dist 75 | apps/spellbound/static -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": false 4 | } 5 | -------------------------------------------------------------------------------- /.spellbound/description.md: -------------------------------------------------------------------------------- 1 | Spellbound is a VS Code extension that interacts with the OpenAI API to provide an intelligent coding assistant. Spellbound uses an internal conversational dialogue, wherein the underlying LLM agent uses 'tools' to interact with, understand, and perform tasks on your codebase. 2 | -------------------------------------------------------------------------------- /.spellbound/pinecone_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | ".vscode": "" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Launch Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "cwd": "${workspaceFolder}/apps/spellbound/", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}/apps/spellbound/" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/apps/spellbound/dist/**/*.js" 18 | ], 19 | // run rushx build in apps/examples/flow-editor first 20 | // "preLaunchTask": "rush build" 21 | }, 22 | { 23 | "type": "chrome", 24 | "request": "attach", 25 | "name": "Attach WebView", 26 | "port": 9222, 27 | "sourceMaps": true, 28 | "webRoot": "${workspaceFolder}" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off", 13 | "cSpell.words": [ 14 | "Birpc" 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": [ 34 | "npm: watch", 35 | "npm: watch-tests" 36 | ], 37 | "problemMatcher": [] 38 | }, 39 | { 40 | "type": "shell", 41 | "command": "rush build && rush generate-commands", 42 | "label": "rush build", 43 | "problemMatcher": [], 44 | }, 45 | { 46 | "type": "npm", 47 | "script": "start", 48 | "label": "site", 49 | "options": { 50 | "cwd": "${workspaceFolder}/apps/agentitive-site" 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | webpack.config.js 9 | vsc-extension-quickstart.md 10 | **/tsconfig.json 11 | **/.eslintrc.json 12 | **/*.map 13 | **/*.ts 14 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Spellbound Architecture Overview 2 | 3 | The Spellbound codebase can be categorized into multiple parts: 4 | 5 | 1. `apps/spellbound`: This is the main extension folder containing the source code for the Spellbound VS Code extension. 6 | - `src`: Contains the source code of the extension, including commands, tools, and APIs. 7 | - `assets`: Contains the assets used by the extension, such as images and sample markdown files. 8 | 9 | 2. `apps/spellbound-ui`: This folder contains the React-based user interface used in the extension's chatbox. 10 | - `src`: Contains the source code for the UI, including components, utilities, styles, and Redux store. 11 | 12 | 3. `apps/spellbound-shared`: This folder contains shared TypeScript code used by both the extension and the UI. 13 | - `src`: Contains the shared code, such as TypeScript type definitions and the BiDirectional Reactive Procedural Script (biRPC) library for communication between webviews and extensions. 14 | 15 | 4. `apps/agentitive-site`: This folder contains the source code for the documentation site generated with Docusaurus. 16 | - `docs`: Contains markdown files for the documentation. 17 | - `src`: Contains custom components and CSS for the documentation site. 18 | 19 | 5. Other folders and configuration files: 20 | - `.vscode`: Configurations for the workspace. 21 | - `rigs`: Contains various configurations and scripts for the repository. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to the "spellbound" extension will be documented in this file. 4 | 5 | ## 0.0.3 - 2023-04-04 6 | 7 | ### Added 8 | 9 | - Add `search` tool functionality using Pinecone search engine. 10 | - Add related `stats` tool to list Pinecone index metadata. 11 | - Add related VS Code extension commands for workspace indexing. 12 | 13 | ### Fixed 14 | 15 | - Fix CSS for markdown `code` blocks in output. 16 | 17 | ## 0.0.2 - 2023-04-02 18 | 19 | ### Fixed 20 | 21 | - Fix documentation image links in extension marketplace. 22 | 23 | ## 0.0.1 - 2023-04-02 24 | 25 | - Initial release 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Michael Poteat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LOCAL_SETUP.md: -------------------------------------------------------------------------------- 1 | # Local Setup 2 | 3 | This document provides instructions for setting up the local development environment for the Spellbound VS Code extension. 4 | 5 | ## Prerequisites 6 | 7 | - Node.js (v19.x.x recommended) 8 | - Visual Studio Code (v1.77.0 or later) 9 | 10 | ## Rush Monorepo 11 | 12 | The Spellbound repository is a [Rush](https://rushjs.io/) monorepo. This means that the repository contains multiple projects, each of which is a separate Node.js package. Rush uses [pnpm](https://pnpm.io/) under the hood. 13 | 14 | Installing Rush: 15 | 16 | ``` 17 | npm install -g @microsoft/rush pnpm 18 | ``` 19 | 20 | ## Steps to set up the local development environment 21 | 22 | 1. Clone the repository using `git`: 23 | 24 | ``` 25 | git clone https://github.com/agentitive/spellbound.git 26 | ``` 27 | 28 | 2. Change into the cloned directory: 29 | 30 | ``` 31 | cd spellbound 32 | ``` 33 | 34 | 3. Install dependencies: 35 | 36 | ``` 37 | rush update 38 | ``` 39 | 40 | 4. Compile the projects: 41 | 42 | ``` 43 | rush watch 44 | ``` 45 | 46 | 5. Open the project folder in Visual Studio Code: 47 | 48 | ``` 49 | code . 50 | ``` 51 | 52 | 6. Press `F5` to launch the extension in a new VS Code window. 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 |
7 | 8 |

9 | Autonomous and goal-seeking coding agents 10 |

11 | 12 | --- 13 | 14 | **Spellbound** is a [Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=mpoteat-vsce.spellbound) that leverages the power of the OpenAI API to create an intelligent coding assistant. With _Spellbound_, you can harness the capabilities of cutting-edge AI to improve your coding experience and productivity. 15 | 16 | ## Installation 17 | 18 | 1. Install the Spellbound VS Code extension from the marketplace. 19 | 2. Obtain an API key for the OpenAI API and configure it in your VS Code settings. 20 | 3. Obtain a Pinecone API key, create a Pinecone index, and configure it in your VS Code settings. 21 | 22 | ## Usage 23 | 24 | To use Spellbound, simply interact with your codebase as usual. Whenever you need help, type your request in the Spellbound dialog. Spellbound will use the OpenAI API to help you generate code, troubleshoot, and offer suggestions. 25 | 26 | Spellbound uses a "thought/action" control loop to create an autonomous coding agent, as described in the **[ReAct paper](https://arxiv.org/abs/2210.03629)**. The agent is able to take actions based on its current thoughts, and its future thoughts are influenced by the results of its actions. 27 | 28 | ## Extension Settings 29 | 30 | | Parameter | Description | Default | Scope | 31 | | --------------------------- | ---------------------------------------------------- | ------- | ------- | 32 | | spellbound.model | Select the AI model used by the Spellbound extension | gpt-4 | | 33 | | spellbound.openai_api_key | Enter your OpenAI API key here, used for inference. | | machine | 34 | | spellbound.pinecone_api_key | Enter your Pinecone API key here, used for search. | | machine | 35 | | spellbound.pinecone_index | Enter your Pinecone index name here, used for search | | machine | 36 | 37 | ### Configuring Settings 38 | 39 | To configure the extension settings, open the Settings editor in Visual Studio Code by clicking on the gear icon in the lower left corner and selecting "Settings" from the menu. In the search bar, type "Spellbound" to filter the settings related to the extension. You can then enter your API keys and select the preferred model from the available options. 40 | 41 | Environment variables `OPENAI_API_KEY` and `PINECONE_API_KEY` will be used in place of the settings if they are set. 42 | 43 | ## Tools 44 | 45 | The following tools are available for use in Spellbound. The underlying AI model uses tools to solve open-ended tasks. 46 | 47 | | Tool | Description | 48 | | ------------------------ | -------------------------------------------------------------------------------------- | 49 | | cat {path} | Read the content of a file at the given path. | 50 | | ls {path, recursive?} | List files and folders at the given path. | 51 | | search {description} | Search for a file or relevant information by description, via vector embedding search. | 52 | | write {path, contents} | Write (or overwrite) the given contents into the specified file. | 53 | | replace {path, old, new} | Replace all occurrences of `old` with `new` in the specified file. | 54 | | ask {question} | Ask a question to the user. | 55 | | npm {script} | Run an npm script (e.g., `npm run [script]`). | 56 | | done {output?} | Indicate that you are done with the task. | 57 | 58 | ## Features 59 | 60 | - Code generation based on natural language input 61 | - Troubleshooting and bug fixing assistance 62 | - Documentation writing and code critique 63 | - Autonomous and goal-seeking coding agents 64 | - Integration with your existing VS Code workflow 65 | 66 | ## Local Development 67 | 68 | To set up the local development environment for the Spellbound extension, see the [LOCAL_SETUP.md](LOCAL_SETUP.md) file. 69 | 70 | ## License 71 | 72 | Spellbound is released under the MIT License. See the LICENSE file for the full license text. 73 | -------------------------------------------------------------------------------- /apps/agentitive-site/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /apps/agentitive-site/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /apps/agentitive-site/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/agentitive-site/blog/2023-04-05-new-site/docusaurus-plushie-banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentitive/spellbound/99c6e70f2aa74322d074d8b88c0b1cdef2d7acdf/apps/agentitive-site/blog/2023-04-05-new-site/docusaurus-plushie-banner.jpeg -------------------------------------------------------------------------------- /apps/agentitive-site/blog/2023-04-05-new-site/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: new-site 3 | title: New site! 4 | authors: [dustin] 5 | tags: [news] 6 | --- 7 | 8 | Hey, we got a new site! 9 | -------------------------------------------------------------------------------- /apps/agentitive-site/blog/authors.yml: -------------------------------------------------------------------------------- 1 | dustin: 2 | name: Dustin 3 | title: Maintainer 4 | url: https://github.com/dustinlacewell 5 | image_url: https://avatars.githubusercontent.com/u/53952?v=4 6 | -------------------------------------------------------------------------------- /apps/agentitive-site/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Spellbound 6 | 7 | **Spellbound** is a [Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=mpoteat-vsce.spellbound) that leverages the power of the OpenAI API to create an intelligent coding assistant. With _Spellbound_, you can harness the capabilities of cutting-edge AI to improve your coding experience and productivity. 8 | 9 | ## Installation 10 | 11 | 1. Install the Spellbound VS Code extension from the marketplace. 12 | 2. Obtain an API key for the OpenAI API and configure it in your VS Code settings. 13 | 3. Obtain a Pinecone API key, create a Pinecone index, and configure it in your VS Code settings. 14 | 15 | ## Usage 16 | 17 | To use Spellbound, simply interact with your codebase as usual. Whenever you need help, type your request in the Spellbound dialog. Spellbound will use the OpenAI API to help you generate code, troubleshoot, and offer suggestions. 18 | 19 | Spellbound uses a "thought/action" control loop to create an autonomous coding agent, as described in the **[ReAct paper](https://arxiv.org/abs/2210.03629)**. The agent is able to take actions based on its current thoughts, and its future thoughts are influenced by the results of its actions. 20 | 21 | ## Extension Settings 22 | 23 | | Parameter | Description | Default | Scope | 24 | | --------------------------- | ---------------------------------------------------- | ------- | ------- | 25 | | spellbound.model | Select the AI model used by the Spellbound extension | gpt-4 | | 26 | | spellbound.openai_api_key | Enter your OpenAI API key here, used for inference. | | machine | 27 | | spellbound.pinecone_api_key | Enter your Pinecone API key here, used for search. | | machine | 28 | | spellbound.pinecone_index | Enter your Pinecone index name here, used for search | | machine | 29 | 30 | ### Configuring Settings 31 | 32 | To configure the extension settings, open the Settings editor in Visual Studio Code by clicking on the gear icon in the lower left corner and selecting "Settings" from the menu. In the search bar, type "Spellbound" to filter the settings related to the extension. You can then enter your API keys and select the preferred model from the available options. 33 | 34 | Environment variables `OPENAI_API_KEY` and `PINECONE_API_KEY` will be used in place of the settings if they are set. 35 | 36 | ## Tools 37 | 38 | The following tools are available for use in Spellbound. The underlying AI model uses tools to solve open-ended tasks. 39 | 40 | | Tool | Description | 41 | | ------------------------ | -------------------------------------------------------------------------------------- | 42 | | cat {path} | Read the content of a file at the given path. | 43 | | ls {path, recursive?} | List files and folders at the given path. | 44 | | search {description} | Search for a file or relevant information by description, via vector embedding search. | 45 | | write {path, contents} | Write (or overwrite) the given contents into the specified file. | 46 | | replace {path, old, new} | Replace all occurrences of `old` with `new` in the specified file. | 47 | | ask {question} | Ask a question to the user. | 48 | | npm {script} | Run an npm script (e.g., `npm run [script]`). | 49 | | done {output?} | Indicate that you are done with the task. | 50 | 51 | ## Features 52 | 53 | - Code generation based on natural language input 54 | - Troubleshooting and bug fixing assistance 55 | - Documentation writing and code critique 56 | - Autonomous and goal-seeking coding agents 57 | - Integration with your existing VS Code workflow 58 | 59 | ## Local Development 60 | 61 | To set up the local development environment for the Spellbound extension, see the [local development docs](local_dev.md) file. 62 | 63 | ## License 64 | 65 | Spellbound is released under the MIT License. See the LICENSE file for the full license text. 66 | -------------------------------------------------------------------------------- /apps/agentitive-site/docs/local_dev.md: -------------------------------------------------------------------------------- 1 | # Local Setup 2 | 3 | This document provides instructions for setting up the local development environment for the Spellbound VS Code extension. 4 | 5 | ## Prerequisites 6 | 7 | - Node.js (v19.x.x recommended) 8 | - Visual Studio Code (v1.77.0 or later) 9 | 10 | ## Rush Monorepo 11 | 12 | The Spellbound repository is a [Rush](https://rushjs.io/) monorepo. This means that the repository contains multiple projects, each of which is a separate Node.js package. Rush uses [pnpm](https://pnpm.io/) under the hood. 13 | 14 | Installing Rush: 15 | 16 | ``` 17 | npm install -g @microsoft/rush pnpm 18 | ``` 19 | 20 | ## Steps to set up the local development environment 21 | 22 | 1. Clone the repository using `git`: 23 | 24 | ``` 25 | git clone https://github.com/agentitive/spellbound.git 26 | ``` 27 | 28 | 2. Change into the cloned directory: 29 | 30 | ``` 31 | cd spellbound 32 | ``` 33 | 34 | 3. Install dependencies: 35 | 36 | ``` 37 | rush update 38 | ``` 39 | 40 | 4. Compile the projects: 41 | 42 | ``` 43 | rush watch 44 | ``` 45 | 46 | 5. Open the project folder in Visual Studio Code: 47 | 48 | ``` 49 | code . 50 | ``` 51 | 52 | 6. Press `F5` to launch the extension in a new VS Code window. 53 | -------------------------------------------------------------------------------- /apps/agentitive-site/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 5 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: 'agentitive.io', 10 | tagline: 'Autonomous goal-seeking agents', 11 | favicon: 'img/favicon.ico', 12 | 13 | // Set the production url of your site here 14 | url: 'https://agentitive.io', 15 | // Set the // pathname under which your site is served 16 | // For GitHub pages deployment, it is often '//' 17 | baseUrl: '/', 18 | trailingSlash: false, 19 | 20 | // GitHub pages deployment config. 21 | // If you aren't using GitHub pages, you don't need these. 22 | organizationName: 'agentitive', // Usually your GitHub org/user name. 23 | projectName: 'spellbound', // Usually your repo name. 24 | 25 | onBrokenLinks: 'throw', 26 | onBrokenMarkdownLinks: 'warn', 27 | 28 | // Even if you don't use internalization, you can use this field to set useful 29 | // metadata like html lang. For example, if your site is Chinese, you may want 30 | // to replace "en" with "zh-Hans". 31 | i18n: { 32 | defaultLocale: 'en', 33 | locales: ['en'], 34 | }, 35 | 36 | presets: [ 37 | [ 38 | 'classic', 39 | /** @type {import('@docusaurus/preset-classic').Options} */ 40 | ({ 41 | docs: { 42 | sidebarPath: require.resolve('./sidebars.js'), 43 | }, 44 | blog: { 45 | showReadingTime: true, 46 | }, 47 | theme: { 48 | customCss: require.resolve('./src/css/custom.css'), 49 | }, 50 | }), 51 | ], 52 | ], 53 | 54 | themeConfig: 55 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 56 | ({ 57 | // Replace with your project's social card 58 | image: 'img/docusaurus-social-card.jpg', 59 | navbar: { 60 | title: 'Agentitive.io', 61 | logo: { 62 | alt: 'My Site Logo', 63 | src: 'img/logo.svg', 64 | }, 65 | items: [ 66 | { 67 | type: 'docSidebar', 68 | sidebarId: 'docs', 69 | position: 'left', 70 | label: 'Docs', 71 | }, 72 | {to: '/blog', label: 'Blog', position: 'right'}, 73 | { 74 | href: 'https://github.com/agentitive/spellbound', 75 | label: 'GitHub', 76 | position: 'right', 77 | }, 78 | ], 79 | }, 80 | footer: { 81 | links: [ 82 | { 83 | title: 'Docs', 84 | items: [ 85 | { 86 | label: 'Tutorial', 87 | to: '/docs/intro', 88 | }, 89 | ], 90 | }, 91 | { 92 | title: 'Community', 93 | items: [ 94 | { 95 | label: 'Discord', 96 | href: 'https://discord.gg/rZzrtJtrEP', 97 | } 98 | ], 99 | }, 100 | { 101 | title: 'More', 102 | items: [ 103 | { 104 | label: 'Blog', 105 | to: '/blog', 106 | }, 107 | { 108 | label: 'GitHub', 109 | href: 'https://github.com/agentitive/spellbound', 110 | }, 111 | ], 112 | }, 113 | ], 114 | }, 115 | prism: { 116 | theme: lightCodeTheme, 117 | darkTheme: darkCodeTheme, 118 | }, 119 | }), 120 | }; 121 | 122 | module.exports = config; 123 | -------------------------------------------------------------------------------- /apps/agentitive-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agentitive-site", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.4.0", 19 | "@docusaurus/preset-classic": "2.4.0", 20 | "@mdx-js/react": "^1.6.22", 21 | "clsx": "^1.2.1", 22 | "prism-react-renderer": "^1.3.5", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2" 25 | }, 26 | "devDependencies": { 27 | "@docusaurus/module-type-aliases": "2.4.0", 28 | "@tsconfig/docusaurus": "^1.0.5", 29 | "typescript": "~5.0.3" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.5%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | }, 43 | "engines": { 44 | "node": ">=16.14" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/agentitive-site/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | docs: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | module.exports = sidebars; 34 | -------------------------------------------------------------------------------- /apps/agentitive-site/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Easy to Use', 14 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 15 | description: ( 16 | <> 17 | Docusaurus was designed from the ground up to be easily installed and 18 | used to get your website up and running quickly. 19 | 20 | ), 21 | }, 22 | { 23 | title: 'Focus on What Matters', 24 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 25 | description: ( 26 | <> 27 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 28 | ahead and move your docs into the docs directory. 29 | 30 | ), 31 | }, 32 | { 33 | title: 'Powered by React', 34 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 35 | description: ( 36 | <> 37 | Extend or customize your website layout by reusing React. Docusaurus can 38 | be extended while reusing the same header and footer. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({title, Svg, description}: FeatureItem) { 45 | return ( 46 |
47 |
48 | 49 |
50 |
51 |

{title}

52 |

{description}

53 |
54 |
55 | ); 56 | } 57 | 58 | export default function HomepageFeatures(): JSX.Element { 59 | return ( 60 |
61 |
62 |
63 | {FeatureList.map((props, idx) => ( 64 | 65 | ))} 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /apps/agentitive-site/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /apps/agentitive-site/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | 19 | --ifm-footer-background-color: var(--ifm-header-background-color); 20 | } 21 | 22 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 23 | [data-theme='dark'] { 24 | --ifm-color-primary: #25c2a0; 25 | --ifm-color-primary-dark: #21af90; 26 | --ifm-color-primary-darker: #1fa588; 27 | --ifm-color-primary-darkest: #1a8870; 28 | --ifm-color-primary-light: #29d5b0; 29 | --ifm-color-primary-lighter: #32d8b4; 30 | --ifm-color-primary-lightest: #4fddbf; 31 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 32 | } 33 | -------------------------------------------------------------------------------- /apps/agentitive-site/src/definitions.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.scss'; -------------------------------------------------------------------------------- /apps/agentitive-site/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .main { 7 | /* show img/logo.svg blown up in the background to the left */ 8 | background: url(../../static/img/logo.svg) no-repeat left top; 9 | background-size: 100%; 10 | 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /apps/agentitive-site/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import Layout from '@theme/Layout'; 6 | 7 | import styles from './index.module.css'; 8 | 9 | 10 | export default function Home(): JSX.Element { 11 | const {siteConfig} = useDocusaurusContext(); 12 | return ( 13 | 16 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/agentitive-site/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /apps/agentitive-site/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentitive/spellbound/99c6e70f2aa74322d074d8b88c0b1cdef2d7acdf/apps/agentitive-site/static/.nojekyll -------------------------------------------------------------------------------- /apps/agentitive-site/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentitive/spellbound/99c6e70f2aa74322d074d8b88c0b1cdef2d7acdf/apps/agentitive-site/static/img/favicon.ico -------------------------------------------------------------------------------- /apps/agentitive-site/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/agentitive-site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/spellbound-shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spellbound-shared", 3 | "version": "0.0.3", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc -b" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "node-rig": "workspace:^0.0.1", 15 | "typescript": "~5.0.3", 16 | "@types/node": "~18.15.11", 17 | "@types/vscode": "~1.77.0", 18 | "@types/vscode-webview": "^1.57.0" 19 | }, 20 | "dependencies": { 21 | "zustand": "^4.1.4", 22 | "birpc": "~0.2.10" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/spellbound-shared/src/birpc/birpc.ts: -------------------------------------------------------------------------------- 1 | export type ArgumentsType = T extends (...args: infer A) => any ? A : never 2 | export type ReturnType = T extends (...args: any) => infer R ? R : never 3 | export type PromisifyFn = ReturnType extends Promise 4 | ? T 5 | : (...args: ArgumentsType) => Promise>> 6 | 7 | export type BirpcResolver = (name: string, resolved: Function) => Function | undefined 8 | 9 | export interface ChannelOptions { 10 | /** 11 | * Function to post raw message 12 | */ 13 | post: (data: any, ...extras: any[]) => any | Promise 14 | /** 15 | * Listener to receive raw message 16 | */ 17 | on: (fn: (data: any, ...extras: any[]) => void) => any | Promise 18 | /** 19 | * Custom function to serialize data 20 | * 21 | * by default it passes the data as-is 22 | */ 23 | serialize?: (data: any) => any 24 | /** 25 | * Custom function to deserialize data 26 | * 27 | * by default it passes the data as-is 28 | */ 29 | deserialize?: (data: any) => any 30 | } 31 | 32 | export interface EventOptions { 33 | /** 34 | * Names of remote functions that do not need response. 35 | */ 36 | eventNames?: (keyof Remote)[] 37 | 38 | /** 39 | * Maximum timeout for waiting for response, in milliseconds. 40 | * 41 | * @default 60_000 42 | */ 43 | timeout?: number 44 | 45 | /** 46 | * Custom resolver to resolve function to be called 47 | * 48 | * For advanced use cases only 49 | */ 50 | resolver?: BirpcResolver 51 | 52 | /** 53 | * Custom error handler 54 | */ 55 | onError?: (error: Error, functionName: string, args: any[]) => boolean | void 56 | } 57 | 58 | export type BirpcOptions = EventOptions & ChannelOptions 59 | 60 | export type BirpcFn = PromisifyFn & { 61 | /** 62 | * Send event without asking for response 63 | */ 64 | asEvent(...args: ArgumentsType): void 65 | } 66 | 67 | export interface BirpcGroupFn { 68 | /** 69 | * Call the remote function and wait for the result. 70 | */ 71 | (...args: ArgumentsType): Promise>[]> 72 | /** 73 | * Send event without asking for response 74 | */ 75 | asEvent(...args: ArgumentsType): void 76 | } 77 | 78 | export type BirpcReturn = { 79 | [K in keyof RemoteFunctions]: BirpcFn 80 | } & { $functions: LocalFunctions } 81 | 82 | export type BirpcGroupReturn = { 83 | [K in keyof RemoteFunctions]: BirpcGroupFn 84 | } 85 | 86 | export interface BirpcGroup { 87 | readonly clients: BirpcReturn[] 88 | readonly functions: LocalFunctions 89 | readonly broadcast: BirpcGroupReturn 90 | updateChannels(fn?: ((channels: ChannelOptions[]) => void)): BirpcReturn[] 91 | } 92 | 93 | interface Request { 94 | /** 95 | * Type 96 | */ 97 | t: 'q' 98 | /** 99 | * ID 100 | */ 101 | i?: string 102 | /** 103 | * Method 104 | */ 105 | m: string 106 | /** 107 | * Arguments 108 | */ 109 | a: any[] 110 | } 111 | 112 | interface Response { 113 | /** 114 | * Type 115 | */ 116 | t: 's' 117 | /** 118 | * Id 119 | */ 120 | i: string 121 | /** 122 | * Result 123 | */ 124 | r?: any 125 | /** 126 | * Error 127 | */ 128 | e?: any 129 | } 130 | 131 | type RPCMessage = Request | Response 132 | 133 | export const DEFAULT_TIMEOUT = 60_000 // 1 minute 134 | 135 | const defaultSerialize = (i: any) => i 136 | const defaultDeserialize = defaultSerialize 137 | 138 | export function createBirpc( 139 | functions: LocalFunctions, 140 | options: BirpcOptions, 141 | ): BirpcReturn { 142 | const { 143 | post, 144 | on, 145 | eventNames = [], 146 | serialize = defaultSerialize, 147 | deserialize = defaultDeserialize, 148 | resolver, 149 | timeout = DEFAULT_TIMEOUT, 150 | } = options 151 | 152 | const rpcPromiseMap = new Map() 153 | 154 | let _promise: Promise | any 155 | 156 | const rpc = new Proxy({}, { 157 | get(_, method: string) { 158 | if (method === '$functions') 159 | return functions 160 | 161 | const sendEvent = (...args: any[]) => { 162 | post(serialize({ m: method, a: args, t: 'q' })) 163 | } 164 | if (eventNames.includes(method as any)) { 165 | sendEvent.asEvent = sendEvent 166 | return sendEvent 167 | } 168 | const sendCall = async (...args: any[]) => { 169 | // Wait if `on` is promise 170 | await _promise 171 | return new Promise((resolve, reject) => { 172 | const id = nanoid() 173 | rpcPromiseMap.set(id, { resolve, reject }) 174 | post(serialize({ m: method, a: args, i: id, t: 'q' })) 175 | if (timeout >= 0) { 176 | setTimeout(() => { 177 | reject(new Error(`[birpc] timeout on calling "${method}"`)) 178 | rpcPromiseMap.delete(id) 179 | }, timeout) 180 | } 181 | }) 182 | } 183 | sendCall.asEvent = sendEvent 184 | return sendCall 185 | }, 186 | }) as BirpcReturn 187 | 188 | _promise = on(async (data, ...extra) => { 189 | const msg = deserialize(data) as RPCMessage 190 | if (msg.t === 'q') { 191 | const { m: method, a: args } = msg 192 | let result, error: any 193 | const fn = resolver 194 | ? resolver(method, (functions as any)[method]) 195 | : (functions as any)[method] 196 | 197 | if (!fn) { 198 | error = new Error(`[birpc] function "${method}" not found`) 199 | } 200 | else { 201 | try { 202 | result = await fn.apply(rpc, args) 203 | } 204 | catch (e) { 205 | error = e 206 | } 207 | } 208 | 209 | if (msg.i) { 210 | if (error && options.onError) 211 | options.onError(error, method, args) 212 | post(serialize({ t: 's', i: msg.i, r: result, e: error }), ...extra) 213 | } 214 | } 215 | else { 216 | const { i: ack, r: result, e: error } = msg 217 | const promise = rpcPromiseMap.get(ack) 218 | if (promise) { 219 | if (error) 220 | promise.reject(error) 221 | else 222 | promise.resolve(result) 223 | } 224 | rpcPromiseMap.delete(ack) 225 | } 226 | }) 227 | 228 | return rpc 229 | } 230 | 231 | const cacheMap = new WeakMap() 232 | export function cachedMap(items: T[], fn: ((i: T) => R)): R[] { 233 | return items.map((i) => { 234 | let r = cacheMap.get(i) 235 | if (!r) { 236 | r = fn(i) 237 | cacheMap.set(i, r) 238 | } 239 | return r 240 | }) 241 | } 242 | 243 | export function createBirpcGroup( 244 | functions: LocalFunctions, 245 | channels: ChannelOptions[] | (() => ChannelOptions[]), 246 | options: EventOptions = {}, 247 | ): BirpcGroup { 248 | const getChannels = () => typeof channels === 'function' ? channels() : channels 249 | const getClients = (channels = getChannels()) => cachedMap(channels, s => createBirpc(functions, { ...options, ...s })) 250 | 251 | const broadcastProxy = new Proxy({}, { 252 | get(_, method) { 253 | const client = getClients() 254 | const callbacks = client.map(c => (c as any)[method]) 255 | const sendCall = (...args: any[]) => { 256 | return Promise.all(callbacks.map(i => i(...args))) 257 | } 258 | sendCall.asEvent = (...args: any[]) => { 259 | callbacks.map(i => i.asEvent(...args)) 260 | } 261 | return sendCall 262 | }, 263 | }) as BirpcGroupReturn 264 | 265 | function updateChannels(fn?: ((channels: ChannelOptions[]) => void)) { 266 | const channels = getChannels() 267 | fn?.(channels) 268 | return getClients(channels) 269 | } 270 | 271 | getClients() 272 | 273 | return { 274 | get clients() { 275 | return getClients() 276 | }, 277 | functions, 278 | updateChannels, 279 | broadcast: broadcastProxy, 280 | /** 281 | * @deprecated use `broadcast` 282 | */ 283 | // @ts-expect-error deprecated 284 | boardcast: broadcastProxy, 285 | } 286 | } 287 | 288 | // port from nanoid 289 | // https://github.com/ai/nanoid 290 | const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' 291 | function nanoid(size = 21) { 292 | let id = '' 293 | let i = size 294 | while (i--) 295 | id += urlAlphabet[(Math.random() * 64) | 0] 296 | return id 297 | } -------------------------------------------------------------------------------- /apps/spellbound-shared/src/birpc/extension.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "../types" 2 | 3 | 4 | export type ExtensionProcedures = { 5 | abort(): Promise, 6 | submit(messages: Message[]): Promise, 7 | saveToFile(messages: Message[]): Promise, 8 | } 9 | -------------------------------------------------------------------------------- /apps/spellbound-shared/src/birpc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './birpc'; 2 | export * from './extension'; 3 | export * from './webview'; 4 | -------------------------------------------------------------------------------- /apps/spellbound-shared/src/birpc/webview.ts: -------------------------------------------------------------------------------- 1 | export type WebviewProcedures = { 2 | start(): Promise, 3 | update(message: string): Promise, 4 | finish(): Promise, 5 | result(message: string): Promise, 6 | } 7 | -------------------------------------------------------------------------------- /apps/spellbound-shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './birpc'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /apps/spellbound-shared/src/types/Message.ts: -------------------------------------------------------------------------------- 1 | export type Message = { 2 | role: string 3 | content: string 4 | } -------------------------------------------------------------------------------- /apps/spellbound-shared/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Message'; 2 | -------------------------------------------------------------------------------- /apps/spellbound-shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/node-rig/profiles/default/tsconfig-base.json", 3 | "compilerOptions": { 4 | "types": ["node", "vscode"] 5 | } 6 | } -------------------------------------------------------------------------------- /apps/spellbound-ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /apps/spellbound-ui/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "exportall.config.folderListener": [ 3 | "/src/components/Util" 4 | ], 5 | 6 | // four spaces 7 | "editor.tabSize": 4, 8 | "editor.detectIndentation": false, 9 | 10 | // format on save 11 | "editor.formatOnSave": true, 12 | // fix imports 13 | "editor.codeActionsOnSave": { 14 | "source.fixAll.eslint": true 15 | }, 16 | 17 | } -------------------------------------------------------------------------------- /apps/spellbound-ui/README.md: -------------------------------------------------------------------------------- 1 | # `webview-ui` Directory 2 | 3 | This directory contains all of the code that will be executed within the webview context. It can be thought of as the place where all the "frontend" code of a webview is contained. 4 | 5 | Types of content that can be contained here: 6 | 7 | - Frontend framework code (i.e. React, Svelte, Vue, etc.) 8 | - JavaScript files 9 | - CSS files 10 | - Assets / resources (i.e. images, illustrations, etc.) 11 | -------------------------------------------------------------------------------- /apps/spellbound-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spellbound-ui", 3 | "version": "0.0.3", 4 | "private": true, 5 | "scripts": { 6 | "start": "EXTEND_ESLINT=true react-scripts start", 7 | "build": "DISABLE_ESLINT_PLUGIN=true node ./scripts/build-react-no-split.js", 8 | "watch": "DISABLE_ESLINT_PLUGIN=true node ./scripts/build-react-no-split.js", 9 | "test": "EXTEND_ESLINT=true react-scripts test", 10 | "eject": "react-scripts eject" 11 | }, 12 | "watch": { 13 | "build": { 14 | "patterns": [ 15 | "src" 16 | ], 17 | "extensions": "ts,tsx,scss" 18 | } 19 | }, 20 | "dependencies": { 21 | "@fortawesome/fontawesome-svg-core": "~6.4.0", 22 | "@fortawesome/free-solid-svg-icons": "~6.4.0", 23 | "@fortawesome/react-fontawesome": "~0.2.0", 24 | "@vscode/webview-ui-toolkit": "^1.0.0", 25 | "react": "^17.0.2", 26 | "react-dom": "^17.0.2", 27 | "react-draggable": "^4.4.5", 28 | "react-markdown": "^8.0.6", 29 | "react-pie-menu": "1.0.0-alpha.4", 30 | "reactflow": "^11.2.0", 31 | "styled-components": "~5.3.9", 32 | "zustand": "^4.1.4", 33 | "spellbound-shared": "workspace:^0.0.3", 34 | "birpc": "~0.2.10", 35 | "react-autosize-textarea": "~7.1.0" 36 | }, 37 | "devDependencies": { 38 | "@trpc/server": "~10.18.0", 39 | "@types/node": "~18.15.11", 40 | "@types/react": "^17.0.2", 41 | "@types/react-dom": "^17.0.2", 42 | "@types/react-pie-menu": "~1.0.0", 43 | "@types/styled-components": "~5.1.26", 44 | "@types/vscode-webview": "^1.57.0", 45 | "react-scripts": "^5.0.1", 46 | "rewire": "^5.0.0", 47 | "sass": "~1.60.0", 48 | "typescript": "~5.0.3" 49 | }, 50 | "eslintConfig": { 51 | "extends": [ 52 | "react-app", 53 | "react-app/jest" 54 | ], 55 | "rules": { 56 | "no-unused-expressions": "off" 57 | } 58 | }, 59 | "browserslist": { 60 | "production": [ 61 | ">0.2%", 62 | "not dead", 63 | "not op_mini all" 64 | ], 65 | "development": [ 66 | "last 1 chrome version", 67 | "last 1 firefox version", 68 | "last 1 safari version" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/spellbound-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentitive/spellbound/99c6e70f2aa74322d074d8b88c0b1cdef2d7acdf/apps/spellbound-ui/public/favicon.ico -------------------------------------------------------------------------------- /apps/spellbound-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | @volley/solid-scenes Editor 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /apps/spellbound-ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentitive/spellbound/99c6e70f2aa74322d074d8b88c0b1cdef2d7acdf/apps/spellbound-ui/public/logo192.png -------------------------------------------------------------------------------- /apps/spellbound-ui/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentitive/spellbound/99c6e70f2aa74322d074d8b88c0b1cdef2d7acdf/apps/spellbound-ui/public/logo512.png -------------------------------------------------------------------------------- /apps/spellbound-ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /apps/spellbound-ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/spellbound-ui/scripts/build-react-no-split.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * A script that overrides some of the create-react-app build script configurations 5 | * in order to disable code splitting/chunking and rename the output build files so 6 | * they have no hash. (Reference: https://mtm.dev/disable-code-splitting-create-react-app). 7 | * 8 | * This is crucial for getting React webview code to run because VS Code expects a 9 | * single (consistently named) JavaScript and CSS file when configuring webviews. 10 | */ 11 | 12 | const rewire = require("rewire"); 13 | const defaults = rewire("react-scripts/scripts/build.js"); 14 | const config = defaults.__get__("config"); 15 | 16 | // Disable code splitting 17 | config.optimization.splitChunks = { 18 | cacheGroups: { 19 | default: false, 20 | }, 21 | }; 22 | 23 | // Disable code chunks 24 | config.optimization.runtimeChunk = false; 25 | 26 | // Rename main.{hash}.js to main.js 27 | config.output.filename = "static/js/[name].js"; 28 | 29 | // Rename main.{hash}.css to main.css 30 | config.plugins[5].options.filename = "static/css/[name].css"; 31 | config.plugins[5].options.moduleFilename = () => "static/css/main.css"; 32 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/backup.ts: -------------------------------------------------------------------------------- 1 | export default null 2 | 3 | // (function () { 4 | // const vscode = acquireVsCodeApi(); 5 | 6 | // const sendButton = document.querySelector('#input-area button'); 7 | // const inputField = document.querySelector('#input-area input'); 8 | // const messageHistoryContainer = document.querySelector("#message-history"); 9 | 10 | // const messages = [] 11 | 12 | // sendButton.addEventListener("click", () => { 13 | // const prompt = inputField.value.trim() 14 | // if (prompt) { 15 | // // Pass the prompt to the extension 16 | // vscode.postMessage({ 17 | // command: "sendPrompt", 18 | // messages: [...messages, { role: "system", content: prompt }], 19 | // }) 20 | // inputField.value = "" 21 | // } 22 | // }) 23 | 24 | // inputField.addEventListener('keypress', (event) => { 25 | // if (event.key === 'Enter') { 26 | // event.preventDefault(); 27 | // sendButton.click(); 28 | // } 29 | // }); 30 | 31 | // function addMessage(message) { 32 | // const messageElement = document.createElement('div'); 33 | // messageElement.className = message.type === 'input' ? 'user-message' : 'response-message'; 34 | // messageElement.textContent = message.content; 35 | // messageHistoryContainer.appendChild(messageElement); 36 | 37 | // if (message.type === 'input') { 38 | // messageElement.innerHTML = message.rendered; 39 | // } 40 | 41 | // if (message.type === 'start' || message.type === 'input') { 42 | // messages.push({ 43 | // role: message.type === 'input' ? 'system' : 'assistant', 44 | // content: message.content ?? "", 45 | // }) 46 | // } 47 | // } 48 | 49 | // // Add a message event listener to handle 'addMessage' command 50 | // window.addEventListener('message', (event) => { 51 | // const message = event.data; // The JSON data our extension sent 52 | // switch (message.command) { 53 | // case 'sendMessage': 54 | // switch(message.message.type) { 55 | // case 'start': 56 | // addMessage(message.message); 57 | // break; 58 | // case 'chunk': 59 | // const lastResponse = document.querySelector('.response-message:last-child'); 60 | 61 | // lastResponse.innerHTML = message.message.rendered; 62 | 63 | // messages[messages.length - 1].content += message.message.content; 64 | // break; 65 | // case 'done': 66 | // // Todo: re-enable the input area 67 | // break; 68 | // case 'input': 69 | // addMessage(message.message); 70 | // break; 71 | // case 'tool-result': 72 | // // Treat the tool result as a prompt (implement 'thought-loop') 73 | // vscode.postMessage({ 74 | // command: "sendPrompt", 75 | // messages: [...messages, { role: "system", content: message.message.content }] 76 | // }) 77 | // break; 78 | // } 79 | // break; 80 | // } 81 | // }); 82 | // })();` -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/App/index.tsx: -------------------------------------------------------------------------------- 1 | import { InputPanel } from "../InputPanel"; 2 | import { MessageHistory } from "../MessageHistory"; 3 | import styles from "./styles.module.scss" 4 | 5 | 6 | export function App() { 7 | return ( 8 |
9 |

Spellbound 💫

10 | 11 | 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/App/styles.module.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | height: 100%; 3 | display: "flex"; 4 | flex-direction: column; 5 | justify-content: "stretch"; 6 | align-items: "stretch"; 7 | 8 | & > h1 { 9 | text-align: center; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/InputPanel/InputPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import AutosizeTextArea from "react-autosize-textarea"; 3 | 4 | import styles from "./styles.module.scss" 5 | 6 | import { rpc } from "../../utilities/rpc"; 7 | import useStore from "../../store"; 8 | 9 | export function InputPanel() { 10 | const { addMessage, messages, isThinking, setIsThinking } = useStore(state => ({ 11 | addMessage: state.addMessage, 12 | messages: state.messages, 13 | isThinking: state.isThinking, 14 | setIsThinking: state.setIsThinking, 15 | })) 16 | const [input, setInput] = useState(""); 17 | 18 | const onClick = async () => { 19 | const prompt = input.trim() 20 | if (prompt) { 21 | const newMessage = { role: "user", content: prompt } 22 | addMessage(newMessage) 23 | const newMessages = [...messages, newMessage] 24 | console.log("submitting new messages...") 25 | await rpc.submit(newMessages) 26 | setInput("") 27 | } 28 | } 29 | 30 | const onAbort = async () => { 31 | await rpc.abort() 32 | setIsThinking(false) 33 | } 34 | 35 | const onKeyDown = (e: React.KeyboardEvent) => { 36 | if (e.key === "Enter") { 37 | e.preventDefault() 38 | onClick() 39 | } 40 | } 41 | 42 | const saveToFile = async () => { 43 | await rpc.saveToFile(messages) 44 | } 45 | 46 | return ( 47 |
48 | ) => setInput(e.currentTarget.value)} 54 | /> 55 | 60 | {isThinking && } 65 | {!isThinking && } 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/InputPanel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './InputPanel'; 2 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/InputPanel/styles.module.scss: -------------------------------------------------------------------------------- 1 | .inputPanel { 2 | position: absolute; 3 | bottom: 0; 4 | width: 95%; 5 | padding: 8px; 6 | display: flex; 7 | 8 | & > textarea { 9 | flex-grow: 1; 10 | outline: none; 11 | border: none; 12 | border-radius: 2px; 13 | padding: 6px; 14 | font-size: 14px; 15 | color: #CCC; 16 | background-color: #0e1116; 17 | resize: none; 18 | font: inherit; 19 | } 20 | 21 | & > input:focus { 22 | border: 1px solid #007fd4; 23 | } 24 | 25 | & > button { 26 | border: none; 27 | color: white; 28 | padding: 6px 12px; 29 | cursor: pointer; 30 | margin-left: 4px; 31 | font-size: 14px; 32 | border-radius: 2px; 33 | outline: none; 34 | } 35 | 36 | & > .send { 37 | background-color: #438440; 38 | } 39 | 40 | & > .send:hover { 41 | background-color: #488848; 42 | } 43 | 44 | & > .export { 45 | background-color: #000000; 46 | } 47 | 48 | & > .export:hover { 49 | background-color: #111111; 50 | } 51 | 52 | & > button:disabled { 53 | background-color: #666; 54 | cursor: not-allowed; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/MessageHistory/MessageHistory.tsx: -------------------------------------------------------------------------------- 1 | import useStore from "../../store" 2 | import { MessageItem } from "../MessageItem" 3 | 4 | import styles from "./styles.module.scss" 5 | 6 | 7 | export const MessageHistory = () => { 8 | const { messages } = useStore(state => ({ messages: state.messages })) 9 | 10 | return
11 | {messages.map((message, index) => ( 12 | 13 | ))} 14 |
15 | } -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/MessageHistory/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MessageHistory'; 2 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/MessageHistory/styles.module.scss: -------------------------------------------------------------------------------- 1 | .messageHistory { 2 | position: relative; 3 | overflow-y: auto; 4 | padding: 16px; 5 | height: calc(100vh - 145px); 6 | } -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/MessageItem/MessageItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | 3 | import ReactMarkdown from 'react-markdown' 4 | 5 | import styles from "./styles.module.scss" 6 | import { Message } from "spellbound-shared" 7 | 8 | 9 | export type MessageItemProps = { 10 | message: Message 11 | } 12 | 13 | const styleForRole = (role: string) => { 14 | switch (role) { 15 | case "user": 16 | return styles.user 17 | case "assistant": 18 | return styles.assistant 19 | case "system": 20 | return styles.system 21 | } 22 | } 23 | 24 | 25 | export const MessageItem: FC = ({ message }) => { 26 | return
27 | {/*
{message.role}
*/} 28 |
29 | 30 |
31 |
32 | } -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/MessageItem/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MessageItem'; 2 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/MessageItem/styles.module.scss: -------------------------------------------------------------------------------- 1 | .system { 2 | background-color: rgba(255, 255, 255, 0.2); 3 | color: white; 4 | } 5 | 6 | .user { 7 | background-color: rgba(255, 255, 255, 0.1); 8 | color: rgb(220, 220, 220); 9 | } 10 | 11 | .assistant { 12 | background-color: rgba(255, 255, 255, 0.2); 13 | color: rgb(236, 236, 236); 14 | } 15 | 16 | .messageItem { 17 | display: flex; 18 | flex-direction: column; 19 | 20 | padding: 8px; 21 | margin-bottom: 8px; 22 | border-radius: 4px; 23 | 24 | 25 | .role { 26 | display: flex; 27 | flex-direction: row; 28 | justify-content: flex-start; 29 | align-items: center; 30 | } 31 | 32 | .content { 33 | display: flex; 34 | flex-direction: column; 35 | overflow-x: auto; 36 | } 37 | } -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './App'; 2 | export * from './styles'; 3 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/components/styles.ts: -------------------------------------------------------------------------------- 1 | export const max = { 2 | height: "100vh", 3 | display: "flex", 4 | justifyContent: "stretch", 5 | alignItems: "stretch", 6 | } 7 | 8 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { App } from "./components"; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById("root") 10 | ); 11 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { Message } from 'spellbound-shared/src/types'; 3 | 4 | 5 | export type DocumentState = { 6 | isThinking: boolean, 7 | messages: Message[], 8 | 9 | setIsThinking(isThinking: boolean): void, 10 | clearMessages(): void, 11 | addMessage(message: Message): void, 12 | updateMessage(index: number, content: string): void, 13 | }; 14 | 15 | // this is our useStore hook that we can use in our components to get parts of the store and call actions 16 | const useStore = create((set, get) => ({ 17 | isThinking: false, 18 | messages: [], 19 | 20 | setIsThinking: (isThinking: boolean) => set({ isThinking }), 21 | clearMessages: () => set({ messages: [] }), 22 | addMessage: (message: Message) => set(state => ({ messages: [...state.messages, message] })), 23 | updateMessage: (index: number, content: string) => set(state => { 24 | const messages = [...state.messages]; 25 | messages[index] = { role: messages[index].role, content }; 26 | return { messages }; 27 | }), 28 | })); 29 | 30 | export default useStore; -------------------------------------------------------------------------------- /apps/spellbound-ui/src/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | pre { white-space: pre-wrap; word-wrap: break-word; } 3 | code { display: block; padding: 0.5em; overflow-x: auto; } 4 | 5 | 6 | 7 | #message-history { 8 | position: relative; 9 | overflow-y: auto; 10 | padding: 16px; 11 | height: calc(100% - 70px); 12 | } 13 | 14 | #input-area { 15 | position: absolute; 16 | bottom: 0; 17 | width: 95%; 18 | padding: 8px; 19 | display: flex; 20 | } 21 | 22 | #input-area input { 23 | flex-grow: 1; 24 | outline: none; 25 | border: none; 26 | border-radius: 2px; 27 | padding: 6px; 28 | font-size: 14px; 29 | color: #CCC; 30 | background-color: #0e1116; 31 | } 32 | 33 | #input-area input:focus { 34 | border: 1px solid #007fd4; 35 | } 36 | 37 | #input-area button { 38 | background-color: #438440; 39 | border: none; 40 | color: white; 41 | padding: 6px 12px; 42 | cursor: pointer; 43 | margin-left: 4px; 44 | font-size: 14px; 45 | border-radius: 2px; 46 | outline: none; 47 | } 48 | 49 | #input-area button:hover { 50 | background-color: #488848; 51 | } 52 | 53 | .user-message { 54 | background-color: rgba(255, 255, 255, 0.1); 55 | padding: 8px; 56 | margin-bottom: 8px; 57 | border-radius: 4px; 58 | } 59 | 60 | .response-message { 61 | background-color: rgba(255, 255, 255, 0.2); 62 | padding: 8px; 63 | margin-bottom: 8px; 64 | border-radius: 4px; 65 | } -------------------------------------------------------------------------------- /apps/spellbound-ui/src/utilities/rpc.ts: -------------------------------------------------------------------------------- 1 | import { vscode } from "./vscode"; 2 | import useStore from "../store"; 3 | import { ExtensionProcedures, createBirpc } from "spellbound-shared"; 4 | 5 | export const rpc = createBirpc({ 6 | start: async () => { 7 | const { setIsThinking, addMessage } = useStore.getState(); 8 | 9 | setIsThinking(true); 10 | addMessage({ 11 | role: 'assistant', 12 | content: '' 13 | }); 14 | }, 15 | update: async (message: string) => { 16 | const { messages, updateMessage } = useStore.getState(); 17 | const lastMessage = messages[messages.length - 1]; 18 | updateMessage(messages.length - 1, lastMessage.content + message) 19 | }, 20 | finish: async () => { 21 | const { setIsThinking } = useStore.getState(); 22 | setIsThinking(false); 23 | }, 24 | result: async (message: string) => { 25 | const { addMessage } = useStore.getState(); 26 | addMessage({ 27 | role: 'system', 28 | content: message 29 | }) 30 | }, 31 | }, { 32 | post: data => { 33 | vscode.postMessage(data) 34 | }, 35 | on: cb => { 36 | window.addEventListener('message', event => { 37 | cb(event.data) 38 | }) 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /apps/spellbound-ui/src/utilities/vscode.ts: -------------------------------------------------------------------------------- 1 | import type { WebviewApi } from "vscode-webview"; 2 | 3 | /** 4 | * A utility wrapper around the acquireVsCodeApi() function, which enables 5 | * message passing and state management between the webview and extension 6 | * contexts. 7 | * 8 | * This utility also enables webview code to be run in a web browser-based 9 | * dev server by using native web browser features that mock the functionality 10 | * enabled by acquireVsCodeApi. 11 | */ 12 | class VSCodeAPIWrapper { 13 | private readonly vsCodeApi: WebviewApi | undefined; 14 | 15 | constructor() { 16 | // Check if the acquireVsCodeApi function exists in the current development 17 | // context (i.e. VS Code development window or web browser) 18 | if (typeof acquireVsCodeApi === "function") { 19 | this.vsCodeApi = acquireVsCodeApi(); 20 | } 21 | } 22 | 23 | /** 24 | * Post a message (i.e. send arbitrary data) to the owner of the webview. 25 | * 26 | * @remarks When running webview code inside a web browser, postMessage will instead 27 | * log the given message to the console. 28 | * 29 | * @param message Abitrary data (must be JSON serializable) to send to the extension context. 30 | */ 31 | public postMessage(message: unknown) { 32 | if (this.vsCodeApi) { 33 | this.vsCodeApi.postMessage(message); 34 | } else { 35 | console.log(message); 36 | } 37 | } 38 | 39 | /** 40 | * Get the persistent state stored for this webview. 41 | * 42 | * @remarks When running webview source code inside a web browser, getState will retrieve state 43 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 44 | * 45 | * @return The current state or `undefined` if no state has been set. 46 | */ 47 | public getState(): unknown | undefined { 48 | if (this.vsCodeApi) { 49 | return this.vsCodeApi.getState(); 50 | } else { 51 | const state = localStorage.getItem("vscodeState"); 52 | return state ? JSON.parse(state) : undefined; 53 | } 54 | } 55 | 56 | /** 57 | * Set the persistent state stored for this webview. 58 | * 59 | * @remarks When running webview source code inside a web browser, setState will set the given 60 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 61 | * 62 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved 63 | * using {@link getState}. 64 | * 65 | * @return The new state. 66 | */ 67 | public setState(newState: T): T { 68 | if (this.vsCodeApi) { 69 | return this.vsCodeApi.setState(newState); 70 | } else { 71 | localStorage.setItem("vscodeState", JSON.stringify(newState)); 72 | return newState; 73 | } 74 | } 75 | } 76 | 77 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi. 78 | export const vscode = new VSCodeAPIWrapper(); 79 | -------------------------------------------------------------------------------- /apps/spellbound-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "strictPropertyInitialization": false 19 | }, 20 | "include": ["src", "src/store.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /apps/spellbound-ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require("mini-css-extract-plugin") 2 | 3 | module.exports = { 4 | mode: "production", 5 | entry: "./src/index.tsx", 6 | output: { 7 | path: __dirname + "/build", 8 | publicPath: "/", 9 | filename: "static/js/main.js", 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | use: { 17 | loader: "babel-loader", 18 | }, 19 | }, 20 | { 21 | test: /\.css$/, 22 | use: [MiniCssExtractPlugin.loader, "css-loader"], 23 | }, 24 | ], 25 | }, 26 | plugins: [ 27 | new MiniCssExtractPlugin({ 28 | filename: "static/css/main.css", 29 | }), 30 | ], 31 | optimization: { 32 | splitChunks: { 33 | cacheGroups: { 34 | default: false, 35 | }, 36 | }, 37 | runtimeChunk: false, 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /apps/spellbound/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | -------------------------------------------------------------------------------- /apps/spellbound/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/naming-convention": "off", 11 | "@typescript-eslint/semi": ["warn", "never"], 12 | "curly": "warn", 13 | "eqeqeq": "warn", 14 | "no-throw-literal": "warn", 15 | "semi": "off" 16 | }, 17 | "ignorePatterns": ["out", "dist", "**/*.d.ts"], 18 | "overrides": [ 19 | { 20 | "files": ["*.json"], 21 | "rules": { 22 | "@typescript-eslint/naming-convention": "off" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /apps/spellbound/.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't allow people to merge changes to these generated files, because the result 2 | # may be invalid. You need to run "rush update" again. 3 | pnpm-lock.yaml merge=text 4 | shrinkwrap.yaml merge=binary 5 | npm-shrinkwrap.json merge=binary 6 | yarn.lock merge=binary 7 | 8 | # Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic 9 | # syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor 10 | # may also require a special configuration to allow comments in JSON. 11 | # 12 | # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 13 | # 14 | *.json linguist-language=JSON-with-Comments 15 | -------------------------------------------------------------------------------- /apps/spellbound/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .DS_Store 7 | static -------------------------------------------------------------------------------- /apps/spellbound/.spellbound/description.md: -------------------------------------------------------------------------------- 1 | This is the Spellbound VSCode extension repo. It's a typescript project. The code is in src/ It uses a webview to offer a chat-like experience for interacting with the Spellbound agent, which is you. 2 | 3 | You can test code changes with `npm run build` -------------------------------------------------------------------------------- /apps/spellbound/.spellbound/pinecone_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | ".vscode": "" 3 | } 4 | -------------------------------------------------------------------------------- /apps/spellbound/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Extension", 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "cwd": "${workspaceFolder}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | // run rushx build in apps/examples/flow-editor first 19 | // "preLaunchTask": "rush build" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /apps/spellbound/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentitive/spellbound/99c6e70f2aa74322d074d8b88c0b1cdef2d7acdf/apps/spellbound/assets/icon.png -------------------------------------------------------------------------------- /apps/spellbound/assets/prompt.md: -------------------------------------------------------------------------------- 1 | You are an AI agent that needs to complete a high-level task in an unknown codebase using Thought-Action chain-of-reasoning. 2 | 3 | 1. Your responses always have both "## Thought" and "## Action". 4 | 2. The "## Thought" contains your reasoning, subgoals and action explanation. 5 | 3. The "## Action" contains a YAML structure with 'tool' key and any other required arguments. 6 | 4. Only one tool can be used per interaction, with "done" marking final conclusion. 7 | 8 | **Codebase Description**: <%= codebase_description %> 9 | 10 | **Language**: <%= language %> 11 | 12 | **Current File Name**: <%= current_file_name %> 13 | 14 | Available Actions: 15 | 16 | <%= toolList %> 17 | 18 | EXAMPLE THOUGHT/ACTION PAIR: 19 | 20 | ## Thought 21 | 22 | I need to find the file that contains the function that is called when the user clicks on the "Submit" button. 23 | 24 | ## Action 25 | 26 | ```yaml 27 | tool: grep 28 | regex: "submit" 29 | ``` 30 | 31 | CONVERSATION: 32 | -------------------------------------------------------------------------------- /apps/spellbound/assets/wand-magic-sparkles-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/spellbound/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spellbound", 3 | "displayName": "spellbound", 4 | "description": "Use OpenAI models to weave the magic of AI into your coding experience, generating enchanting solutions and sparking intuitive insights.", 5 | "license": "MIT", 6 | "publisher": "mpoteat-vsce", 7 | "icon": "./assets/icon.png", 8 | "galleryBanner.color": "#0e1116", 9 | "version": "0.0.4", 10 | "engines": { 11 | "vscode": "^1.77.0" 12 | }, 13 | "categories": [ 14 | "Other" 15 | ], 16 | "activationEvents": [ 17 | "onStartupFinished" 18 | ], 19 | "main": "./dist/extension.js", 20 | "repository": { 21 | "type": "https", 22 | "url": "https://github.com/agentitive/spellbound.git" 23 | }, 24 | "contributes": { 25 | "menus": { 26 | "explorer/context": [ 27 | { 28 | "command": "spellbound.indexFolder", 29 | "when": "explorerResourceIsFolder" 30 | } 31 | ] 32 | }, 33 | "configuration": { 34 | "title": "Spellbound", 35 | "properties": { 36 | "spellbound.openai_api_key": { 37 | "type": "string", 38 | "default": "", 39 | "description": "Enter your OpenAI API key here, used for inference." 40 | }, 41 | "spellbound.pinecone_api_key": { 42 | "type": "string", 43 | "default": "", 44 | "description": "Enter your Pinecone API key here, used for search." 45 | }, 46 | "spellbound.pinecone_index": { 47 | "type": "string", 48 | "default": "spellbound", 49 | "description": "Enter your Pinecone index name here, used for search." 50 | }, 51 | "spellbound.model": { 52 | "type": "string", 53 | "default": "gpt-4", 54 | "description": "Select the AI model used by the Spellbound extension." 55 | }, 56 | "spellbound.agent.stopOnError": { 57 | "type": "boolean", 58 | "default": "true", 59 | "description": "Does the AI stop when it encounters an error?" 60 | } 61 | } 62 | }, 63 | "viewsContainers": { 64 | "activitybar": [ 65 | { 66 | "id": "spellbound", 67 | "type": "webview", 68 | "title": "Spellbound", 69 | "icon": "assets/wand-magic-sparkles-solid.svg" 70 | } 71 | ] 72 | }, 73 | "views": { 74 | "spellbound": [ 75 | { 76 | "id": "spellbound.chat", 77 | "type": "webview", 78 | "name": "" 79 | } 80 | ] 81 | }, 82 | "commands": [ 83 | { 84 | "command": "spellbound.executeInstruction", 85 | "title": "Spellbound: Execute Instruction" 86 | }, 87 | { 88 | "command": "spellbound.indexWorkspace", 89 | "title": "Spellbound: Index the current workspace" 90 | }, 91 | { 92 | "command": "spellbound.indexFolder", 93 | "title": "Spellbound: Index the folder contents" 94 | } 95 | ] 96 | }, 97 | "scripts": { 98 | "vscode:prepublish": "npm run package", 99 | "build": "webpack", 100 | "watch": "webpack", 101 | "package": "webpack --mode production --devtool hidden-source-map", 102 | "lint": "eslint src --ext ts" 103 | }, 104 | "devDependencies": { 105 | "@types/ejs": "^3.1.2", 106 | "@types/markdown-it": "^12.2.3", 107 | "@types/node": "~18.15.11", 108 | "@types/vscode": "~1.77.0", 109 | "@typescript-eslint/eslint-plugin": "^5.56.0", 110 | "@typescript-eslint/parser": "^5.56.0", 111 | "eslint": "^8.36.0", 112 | "ts-loader": "^9.4.2", 113 | "typescript": "~5.0.3", 114 | "webpack": "^5.76.3", 115 | "webpack-cli": "^5.0.1", 116 | "copy-webpack-plugin": "~11.0.0", 117 | "spellbound-ui": "workspace:^0.0.3", 118 | "@types/diff": "~5.0.3" 119 | }, 120 | "dependencies": { 121 | "@pinecone-database/pinecone": "~4.0.0", 122 | "spellbound-shared": "workspace:^0.0.3", 123 | "ejs": "^3.1.9", 124 | "ignore": "^5.2.4", 125 | "markdown-it": "^13.0.1", 126 | "node-fetch": "^3.3.1", 127 | "yaml": "^2.2.1", 128 | "diff": "~5.1.0", 129 | "globby": "~13.1.3" 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /apps/spellbound/src/agent/AgentLogicHandler.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | 3 | import { BirpcReturn, Message, WebviewProcedures } from "spellbound-shared" 4 | import { fillPrompt } from "../prompts/fillPrompt" 5 | import YAML from "yaml" 6 | import { AnyToolInterface } from "../tools/AnyToolInterface" 7 | import { ToolEngine } from "../tools/ToolEngine" 8 | import { InferenceService } from "../inference/InferenceHandler" 9 | 10 | export class AgentLogicHandler { 11 | aborted = false 12 | inferences: InferenceService 13 | 14 | constructor(private readonly rpc: BirpcReturn) { 15 | this.inferences = new InferenceService() 16 | } 17 | 18 | async abort() { 19 | this.aborted = true 20 | this.inferences.abort() 21 | await this.rpc.finish() 22 | } 23 | 24 | async promptAgent(messages: Message[]) { 25 | const updatedMessages = await this.createUpdatedMessages(messages) 26 | this.aborted = false 27 | this.inferences.infer( 28 | updatedMessages, 29 | () => this.onStartHandler(), 30 | (data: string) => this.onDataHandler(data), 31 | (result: string) => this.onEndHandler(messages, result) 32 | ) 33 | } 34 | 35 | private async createUpdatedMessages(messages: Message[]): Promise { 36 | const userMessages = messages.filter((m) => m.role === "user") 37 | const filledPrompt = await fillPrompt({ 38 | task: userMessages[userMessages.length - 1].content, 39 | }) 40 | 41 | return [ 42 | { 43 | role: "system", 44 | content: filledPrompt, 45 | }, 46 | ...messages, 47 | ] 48 | } 49 | 50 | private async onStartHandler() { 51 | await this.rpc.start() 52 | } 53 | 54 | private async onDataHandler(output: string) { 55 | await this.rpc.update(output) 56 | } 57 | 58 | private async onEndHandler(messages: Message[], result: string) { 59 | await this.rpc.finish() 60 | 61 | if (this.aborted) { 62 | return 63 | } 64 | 65 | this.handleToolMessage([ 66 | ...messages, 67 | { 68 | role: "assistant", 69 | content: result, 70 | }, 71 | ]) 72 | } 73 | 74 | private async sendResultMessage(messages: Message[], toolResult: string) { 75 | const resultMessage = await this.createResultMessage(toolResult) 76 | await this.rpc.result(resultMessage.content) 77 | 78 | if (!this.aborted) { 79 | // TODO: make this a setting "run until done" 80 | // this causes the agent to respond to result messages 81 | await this.promptAgent([...messages, resultMessage]) 82 | } 83 | } 84 | 85 | private async handleToolMessage(messages: Message[]) { 86 | if (this.aborted) { 87 | return 88 | } 89 | 90 | const actionObject = await this.extractActionObject(messages) 91 | if (actionObject) { 92 | const toolResult = await this.executeToolAction(actionObject) 93 | 94 | if (toolResult) { 95 | await this.sendResultMessage(messages, toolResult) 96 | } 97 | } else { 98 | const warningResult = `No tool action found in message.` 99 | console.warn("warningResult:", messages[messages.length - 1].content) 100 | await this.sendResultMessage(messages, warningResult) 101 | } 102 | } 103 | 104 | private async extractActionObject(messages: Message[]) { 105 | const lastMessage = messages[messages.length - 1] 106 | const actionRegex = /^\s*##\s*Action\s*\n+```[\s\S]*?\n([^]+?)```/gm 107 | const match = actionRegex.exec(lastMessage.content) 108 | 109 | if (match) { 110 | try { 111 | return YAML.parse(match[1], { strict: false }) 112 | } catch (err: any) { 113 | console.error("FULL_MESSAGE", lastMessage.content) 114 | console.error("REGEX_MATCH", match[1]) 115 | console.error("Error parsing tool action:", err) 116 | // get the extension setting .agent.stopOnError 117 | const errorMessage = `ERROR: Could not parse tool action: ${err?.message}` 118 | 119 | const stopOnError = vscode.workspace 120 | .getConfiguration("spellbound") 121 | .get("agent.stopOnError") 122 | if (stopOnError) { 123 | return await this.rpc.result(errorMessage) 124 | } 125 | 126 | await this.sendResultMessage(messages, errorMessage) 127 | } 128 | } 129 | return null 130 | } 131 | 132 | private async createResultMessage(toolResult: string): Promise { 133 | const toolResultOutput = `## Result\n\`\`\`\n${toolResult}\n\`\`\`` 134 | return { 135 | role: "system", 136 | content: toolResultOutput, 137 | } 138 | } 139 | 140 | private async executeToolAction( 141 | actionObject: AnyToolInterface 142 | ): Promise { 143 | if (!actionObject.tool) { 144 | return 145 | } 146 | 147 | try { 148 | const tool = actionObject.tool 149 | const [impl] = ToolEngine[tool] 150 | return await impl(actionObject as never) 151 | } catch (error) { 152 | console.log(error) 153 | throw error 154 | 155 | return `ERROR: Unknown tool: ${(actionObject as any)?.tool}` 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /apps/spellbound/src/agent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AgentLogicHandler'; 2 | -------------------------------------------------------------------------------- /apps/spellbound/src/api/openai.ts: -------------------------------------------------------------------------------- 1 | import * as https from "https" 2 | import { getOpenAIKey } from "../environment/getOpenAIKey" 3 | import { getCurrentModel } from "../environment/getCurrentModel" 4 | import fetch from "node-fetch" 5 | import { Chunk } from "../utils/chunking" 6 | import { Message } from "spellbound-shared" 7 | 8 | type Callbacks = { 9 | onData: (chunk: string) => void 10 | onError?: (error: Error) => void 11 | onEnd?: () => void 12 | } 13 | 14 | type MessageCompletionChunk = { 15 | id: string 16 | object: "chat.completion.chunk" 17 | created: number 18 | model: string 19 | choices: { 20 | delta: { content: string } 21 | index: number 22 | finish_reason: null | string 23 | }[] 24 | } 25 | 26 | function httpRequest(options: https.RequestOptions): Promise { 27 | return new Promise((resolve, reject) => { 28 | const req = https.request(options, (res) => { 29 | let data = "" 30 | 31 | res.on("data", (chunk) => { 32 | data += chunk 33 | }) 34 | 35 | res.on("end", () => { 36 | if (res.statusCode === 200) { 37 | resolve(data) 38 | } else { 39 | reject(new Error(`Request failed. Status code: ${res.statusCode}`)) 40 | } 41 | }) 42 | }) 43 | 44 | req.on("error", reject) 45 | req.end() 46 | }) 47 | } 48 | 49 | export async function listModels(apiKey: string): Promise { 50 | try { 51 | const options: https.RequestOptions = { 52 | hostname: "api.openai.com", 53 | path: "/v1/models", 54 | method: "GET", 55 | headers: { 56 | Authorization: `Bearer ${apiKey}`, 57 | "Content-Type": "application/json", 58 | }, 59 | } 60 | 61 | const data = await httpRequest(options) 62 | const models: string[] = JSON.parse(data).data.map((x: any) => x.id) 63 | return models 64 | } catch (error) { 65 | console.error("Error fetching models:", error) 66 | return [] 67 | } 68 | } 69 | 70 | type PossibleEndpointError = { 71 | error?: { 72 | message?: string | null 73 | type?: string | null 74 | param?: string | null 75 | code?: string | null 76 | } | null 77 | } | null 78 | 79 | const nonRetryErrorTypes = ["tokens", "invalid_request_error"] 80 | 81 | function parseCompletionDataChunk(buffer: Buffer): string { 82 | const contentDelimiter = "\n\n" 83 | const jsonDataPrefix = "data: " 84 | 85 | const bufferString = buffer.toString("utf-8") 86 | const bufferChunks = bufferString.split(contentDelimiter) 87 | 88 | let contentOutput = "" 89 | 90 | for (const chunk of bufferChunks) { 91 | if (chunk.startsWith(jsonDataPrefix)) { 92 | const jsonString = chunk.slice(jsonDataPrefix.length) 93 | 94 | if (jsonString === "[DONE]") { 95 | continue 96 | } 97 | 98 | const jsonData: MessageCompletionChunk = JSON.parse(jsonString) 99 | 100 | for (const choice of jsonData.choices) { 101 | contentOutput += choice.delta.content ?? "" 102 | } 103 | } else { 104 | if (chunk) { 105 | let chunkObj: PossibleEndpointError = null 106 | 107 | try { 108 | chunkObj = JSON.parse(chunk) 109 | } catch (error) { 110 | console.error("Error parsing chunk:", chunk, error) 111 | } 112 | 113 | if (nonRetryErrorTypes.some((x) => x === chunkObj?.error?.type)) { 114 | console.error("Fatal error, not retrying:", chunk) 115 | 116 | throw chunkObj 117 | } 118 | 119 | console.error("Unexpected chunk:", chunk) 120 | } 121 | } 122 | } 123 | 124 | return contentOutput 125 | } 126 | 127 | export async function streamInference( 128 | messages: Message[], 129 | callbacks: Callbacks 130 | ): Promise { 131 | const apiKey = getOpenAIKey() 132 | 133 | const model = await getCurrentModel() 134 | 135 | if (!apiKey || !model) { 136 | return 137 | } 138 | 139 | const options: https.RequestOptions = { 140 | hostname: "api.openai.com", 141 | path: `/v1/chat/completions`, 142 | method: "POST", 143 | headers: { 144 | Authorization: `Bearer ${apiKey}`, 145 | "Content-Type": "application/json", 146 | "Transfer-Encoding": "chunked", 147 | }, 148 | } 149 | 150 | let buffer = "" 151 | 152 | let _resolve: (value: string | PromiseLike) => void 153 | let _reject: (reason?: any) => void 154 | 155 | const req = https.request(options, (res) => { 156 | res.on("data", (chunk) => { 157 | const completion = parseCompletionDataChunk(chunk) 158 | 159 | if (completion) { 160 | buffer += completion 161 | 162 | callbacks.onData(completion) 163 | } 164 | }) 165 | 166 | res.on("error", (error) => { 167 | if (callbacks.onError) { 168 | callbacks.onError(new Error(`Data error: ${error}`)) 169 | } 170 | _reject(error) 171 | }) 172 | 173 | res.on("end", () => { 174 | if (callbacks.onEnd) { 175 | callbacks.onEnd() 176 | } 177 | _resolve(buffer) 178 | }) 179 | }) 180 | 181 | req.on("error", (error) => { 182 | if (callbacks.onError) { 183 | callbacks.onError(new Error(`Request error: ${error}`)) 184 | } 185 | _reject(error) 186 | }) 187 | 188 | req.write(JSON.stringify({ model, messages, stream: true })) 189 | req.end() 190 | 191 | return new Promise((resolve, reject) => { 192 | _resolve = resolve 193 | _reject = reject 194 | }) 195 | } 196 | 197 | export type Embedding = { 198 | embedding: number[] 199 | index: number 200 | object: "embedding" 201 | } 202 | 203 | export type EmbeddingResponse = { 204 | data: Embedding[] 205 | model: string 206 | object: "list" 207 | usage: { 208 | prompt_tokens: number 209 | total_tokens: number 210 | } 211 | } 212 | 213 | export const getEmbedding = async (text: string): Promise => { 214 | const resp = await fetch("https://api.openai.com/v1/embeddings", { 215 | method: "POST", 216 | headers: { 217 | "Content-Type": "application/json", 218 | Authorization: `Bearer ${getOpenAIKey()}`, 219 | }, 220 | body: JSON.stringify({ 221 | input: text, 222 | model: "text-embedding-3-large", 223 | }), 224 | }) 225 | 226 | const data = (await resp.json()) as EmbeddingResponse 227 | 228 | return data.data[0].embedding 229 | } 230 | 231 | export const getEmbeddings = async (texts: string[]): Promise => { 232 | const resp = await fetch("https://api.openai.com/v1/embeddings", { 233 | method: "POST", 234 | headers: { 235 | "Content-Type": "application/json", 236 | Authorization: `Bearer ${getOpenAIKey()}`, 237 | }, 238 | body: JSON.stringify({ 239 | input: texts, 240 | model: "text-embedding-3-large", 241 | }), 242 | }) 243 | 244 | const data = (await resp.json()) as EmbeddingResponse 245 | 246 | if (!data.data) { 247 | return [] 248 | } 249 | 250 | return data.data.map((x) => x.embedding) 251 | } 252 | 253 | export const embedChunk = async (chunk: Chunk) => { 254 | chunk.embedding = await getEmbedding(chunk.text) 255 | } 256 | 257 | export const embedChunks = async (chunks: Chunk[]) => { 258 | const allTexts = chunks.map((chunk) => chunk.text) 259 | console.log(`Embedding chunks...`) 260 | for (let i = 0; i < allTexts.length; i += 1000) { 261 | console.log(` ${i} - ${i + 1000} of ${allTexts.length}`) 262 | const texts = allTexts.slice(i, i + 1000) 263 | const embeddings = await getEmbeddings(texts) 264 | chunks.slice(i, i + 1000).forEach((chunk, j) => { 265 | chunk.embedding = embeddings[j] 266 | }) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /apps/spellbound/src/api/pinecone.ts: -------------------------------------------------------------------------------- 1 | import { Pinecone } from "@pinecone-database/pinecone" 2 | import { getPineconeIndex } from "../environment/getPineconeIndex" 3 | import { getPineconeKey } from "../environment/getPineconeKey" 4 | import { embedChunks } from "./openai" 5 | import { 6 | Chunk, 7 | IGNORED_EXTENSIONS, 8 | chunkFile, 9 | chunkFiles, 10 | } from "../utils/chunking" 11 | import { createUUID } from "../utils/uuid" 12 | import { runGitCommand } from "../tools/git/utility/runGitCommand" 13 | import { getCurrentWorkspaceFolder } from "../utils/getCurrentWorkspaceFolder" 14 | import path, { join } from "path" 15 | 16 | const createClient = async () => { 17 | const pinecone = new Pinecone({ 18 | apiKey: getPineconeKey()!, 19 | }) 20 | 21 | return pinecone 22 | } 23 | 24 | const getIndex = async () => { 25 | const pinecone = await createClient() 26 | const indexName = getPineconeIndex() 27 | return pinecone.Index(indexName!) 28 | } 29 | 30 | const convertChunk = (chunk: Chunk, metadata = {}) => ({ 31 | id: createUUID(), 32 | values: chunk.embedding!, 33 | metadata: { 34 | ...metadata, 35 | filename: chunk.filename, 36 | start: chunk.start, 37 | end: chunk.end, 38 | text: chunk.text, 39 | }, 40 | }) 41 | 42 | const upsertChunks = async (chunks: Chunk[], namespace = "", metadata = {}) => { 43 | const index = await getIndex() 44 | const vectors = chunks.map(convertChunk, metadata) 45 | console.log(`Upserting chunks:`) 46 | for (let i = 0; i < vectors.length; i += 100) { 47 | console.log(` ${i} - ${i + 100} of ${vectors.length}`) 48 | await index.upsert(vectors.slice(i, i + 100)) 49 | } 50 | } 51 | 52 | export const upsertFile = async ( 53 | filename: string, 54 | block_size: number, 55 | block_overlap: number, 56 | metadata: any, 57 | namespace = "" 58 | ) => { 59 | const chunks = chunkFile(filename, block_size, block_overlap) 60 | await embedChunks(chunks) 61 | await upsertChunks(chunks, namespace, metadata) 62 | } 63 | 64 | export const upsertFolder = async ( 65 | folder: string, 66 | block_size: number, 67 | block_overlap: number, 68 | metadata: any, 69 | namespace = "" 70 | ) => { 71 | const files = (await runGitCommand("ls-files")) 72 | .split("\n") 73 | .slice(1, -1) 74 | .filter( 75 | (x) => 76 | !IGNORED_EXTENSIONS.includes(path.extname(x)) && 77 | !x.endsWith("package-lock.json") 78 | ) 79 | 80 | const workspace = getCurrentWorkspaceFolder() 81 | const absoluteFiles = files.map((f) => join(workspace, f)) 82 | 83 | const chunks = chunkFiles(absoluteFiles, block_size, block_overlap) 84 | await embedChunks(chunks) 85 | await upsertChunks(chunks, namespace, metadata) 86 | } 87 | 88 | export const query = async ( 89 | vector: number[], 90 | topK: number, 91 | filter = undefined 92 | ) => { 93 | const client = await createClient() 94 | const indexName = getPineconeIndex() 95 | const index = client.Index(indexName!) 96 | const queryRequest = { 97 | vector, 98 | topK, 99 | filter, 100 | includeMetadata: true, 101 | includeValues: false, 102 | } 103 | 104 | return index.query(queryRequest) 105 | } 106 | 107 | export const clearNamespace = async () => { 108 | const client = await createClient() 109 | const indexName = getPineconeIndex() 110 | // const index = client.index(indexName!) 111 | 112 | // try { 113 | // await index.deleteAll() 114 | // } catch (error) { 115 | // console.error(error) 116 | // } 117 | } 118 | 119 | // run DescribeIndexStats 120 | export const indexStats = async () => { 121 | const index = await getIndex() 122 | return index.describeIndexStats() 123 | } 124 | -------------------------------------------------------------------------------- /apps/spellbound/src/commands/indexFolder.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { clearNamespace, upsertFolder } from "../api/pinecone" 3 | 4 | // path is vscode Path thing 5 | export const indexFolder = async ( 6 | path: vscode.Uri, 7 | _: any, 8 | namespace: string = "" 9 | ) => { 10 | try { 11 | // if namespace is empty, prompt the user 12 | if (namespace === "") { 13 | namespace = 14 | (await vscode.window.showInputBox({ 15 | prompt: "Enter a namespace for this index", 16 | placeHolder: "namespace", 17 | })) || "" 18 | } 19 | 20 | await clearNamespace() 21 | vscode.window.showInformationMessage(`Indexing ${namespace}...`) 22 | 23 | await upsertFolder(path.path, 4000, 100, {}, namespace) 24 | vscode.window.showInformationMessage("Workspace indexed!") 25 | } catch (error) { 26 | console.error(error) 27 | vscode.window.showErrorMessage("Error indexing workspace") 28 | if (error instanceof Error) { 29 | vscode.window.showErrorMessage(error.message) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/spellbound/src/commands/indexWorkspace.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { clearNamespace, upsertFolder } from "../api/pinecone" 3 | import { makeAbsolute } from "../utils/paths" 4 | 5 | export const indexWorkspace = async () => { 6 | try { 7 | await clearNamespace() 8 | vscode.window.showInformationMessage("Indexing workspace...") 9 | 10 | await upsertFolder(makeAbsolute("."), 100, 10, {}, "code") 11 | vscode.window.showInformationMessage("Workspace indexed!") 12 | } catch (error) { 13 | console.error(error) 14 | vscode.window.showErrorMessage("Error indexing workspace") 15 | if (error instanceof Error) { 16 | vscode.window.showErrorMessage(error.message) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/spellbound/src/environment/getCurrentModel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { getModelList } from "./getModelList" 3 | 4 | let availableModels: string[] = [] 5 | 6 | export async function getCurrentModel() { 7 | const spellboundConfig = vscode.workspace.getConfiguration("spellbound") 8 | 9 | const model = spellboundConfig.get("model") || "gpt-4" 10 | 11 | let updatedListOnThisCheck: boolean = false 12 | 13 | // Refresh the list of available models if it's empty 14 | if (availableModels.length === 0) { 15 | availableModels = await getModelList() 16 | updatedListOnThisCheck = true 17 | } 18 | 19 | // Check if the model is available 20 | if (!availableModels.includes(model)) { 21 | if (!updatedListOnThisCheck) { 22 | availableModels = await getModelList() 23 | } 24 | 25 | // If model is not available even after refreshing the list, warn the user 26 | if (!availableModels.includes(model)) { 27 | vscode.window.showWarningMessage( 28 | `Model '${model}' not found. Available models: ${availableModels.join( 29 | ", " 30 | )}\n\nPlease set the 'spellbound.model' setting to one of the available models.` 31 | ) 32 | return 33 | } 34 | } 35 | 36 | return model 37 | } 38 | -------------------------------------------------------------------------------- /apps/spellbound/src/environment/getModelList.ts: -------------------------------------------------------------------------------- 1 | import { listModels } from "../api/openai" 2 | import { getOpenAIKey } from "./getOpenAIKey" 3 | 4 | export async function getModelList() { 5 | const key = getOpenAIKey() 6 | 7 | if (key) { 8 | const models = await listModels(key) 9 | 10 | return models 11 | } 12 | 13 | return [] 14 | } 15 | -------------------------------------------------------------------------------- /apps/spellbound/src/environment/getOpenAIKey.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | 3 | export function getOpenAIKey() { 4 | const openai_api_key = 5 | vscode.workspace 6 | .getConfiguration("spellbound") 7 | .get("openai_api_key") || process.env.OPENAI_API_KEY 8 | 9 | if (!openai_api_key) { 10 | vscode.window.showWarningMessage( 11 | "No OpenAI API key found. Please set the 'spellbound.openai_api_key' setting or set the OPENAI_API_KEY environment variable." 12 | ) 13 | } 14 | 15 | return openai_api_key 16 | } 17 | -------------------------------------------------------------------------------- /apps/spellbound/src/environment/getPineconeIndex.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | 3 | export function getPineconeIndex() { 4 | const pinecone_index = 5 | vscode.workspace 6 | .getConfiguration("spellbound") 7 | .get("pinecone_index") || process.env.PINECONE_INDEX 8 | 9 | if (!pinecone_index) { 10 | vscode.window.showWarningMessage( 11 | "No Pinecone Index found. Please set the 'spellbound.pinecone_index' setting or set the PINECONE_INDEX environment variable." 12 | ) 13 | } 14 | 15 | return pinecone_index 16 | } 17 | -------------------------------------------------------------------------------- /apps/spellbound/src/environment/getPineconeKey.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | 3 | export function getPineconeKey() { 4 | const pinecone_api_key = 5 | vscode.workspace 6 | .getConfiguration("spellbound") 7 | .get("pinecone_api_key") || process.env.PINECONE_API_KEY 8 | 9 | if (!pinecone_api_key) { 10 | vscode.window.showWarningMessage( 11 | "No Pinecone API key found. Please set the 'spellbound.pinecone_api_key' setting or set the PINECONE_API_KEY environment variable." 12 | ) 13 | } 14 | 15 | return pinecone_api_key 16 | } 17 | -------------------------------------------------------------------------------- /apps/spellbound/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { ChatboxViewProvider } from "./views/ChatboxViewProvider" 3 | import { indexWorkspace } from "./commands/indexWorkspace" 4 | import { indexFolder } from "./commands/indexFolder" 5 | 6 | export async function activate(context: vscode.ExtensionContext) { 7 | console.log('Your extension "spellbound" is now active!') 8 | 9 | const chatboxViewProvider = new ChatboxViewProvider(context.extensionUri) 10 | 11 | context.subscriptions.push( 12 | vscode.window.registerWebviewViewProvider( 13 | "spellbound.chat", 14 | chatboxViewProvider, 15 | { 16 | webviewOptions: { 17 | retainContextWhenHidden: true, 18 | }, 19 | } 20 | ) 21 | ) 22 | 23 | // register command 24 | context.subscriptions.push( 25 | vscode.commands.registerCommand("spellbound.indexWorkspace", indexWorkspace) 26 | ) 27 | context.subscriptions.push( 28 | vscode.commands.registerCommand("spellbound.indexFolder", indexFolder) 29 | ) 30 | } 31 | 32 | // This method is called when your extension is deactivated 33 | export function deactivate() {} 34 | -------------------------------------------------------------------------------- /apps/spellbound/src/inference/InferenceHandler.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "spellbound-shared" 2 | import { streamInference } from "../api/openai" 3 | 4 | export class InferenceJob { 5 | private aborted = false 6 | buffer: string 7 | 8 | constructor( 9 | protected messages: Message[], 10 | protected onStart: () => Promise, 11 | protected onData: (output: string) => Promise, 12 | protected onEnd: (result: string) => Promise 13 | ) { 14 | this.buffer = '' 15 | this.aborted = false 16 | } 17 | 18 | abort(): void { 19 | this.aborted = true 20 | } 21 | 22 | public async start(): Promise { 23 | this.buffer = '' 24 | this.aborted = false 25 | 26 | this.onStart() 27 | 28 | await streamInference(this.messages, { 29 | onData: async (output: string) => { 30 | if (!this.aborted) { 31 | this.buffer += output 32 | await this.onData(output) 33 | } 34 | }, 35 | onEnd: async () => { 36 | if (!this.aborted) { 37 | await this.onEnd(this.buffer) 38 | } 39 | }, 40 | }) 41 | } 42 | } 43 | 44 | export class InferenceService { 45 | private currentJob: InferenceJob | null = null 46 | 47 | infer( 48 | messages: Message[], 49 | onStart: () => Promise, 50 | onData: (output: string) => Promise, 51 | onEnd: (result: string) => Promise 52 | ) { 53 | this.abort() 54 | this.currentJob = new InferenceJob(messages, onStart, onData, onEnd) 55 | this.currentJob.start() 56 | } 57 | 58 | abort(): void { 59 | if (this.currentJob) { 60 | this.currentJob.abort() 61 | this.currentJob = null 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /apps/spellbound/src/prompts/fillPrompt.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import ejs from "ejs" 3 | import { promises as fs } from "fs" 4 | import path from "path" 5 | import { AnyToolInterface } from "../tools/AnyToolInterface" 6 | import { ToolEngine } from "../tools/ToolEngine" 7 | 8 | type FillPromptParams = { 9 | codebase_description?: string | undefined 10 | language?: string | undefined 11 | task: string 12 | current_file_name?: string | undefined 13 | langcode?: string | undefined 14 | current_file_contents?: string | undefined 15 | } 16 | 17 | // render unordered list of tools 18 | function renderToolList(tools: Partial) { 19 | return Object.values(tools).map(([_, sig, doc]) => { 20 | return `- ${sig}: ${doc}` 21 | }) 22 | } 23 | 24 | export async function fillPrompt(params?: Partial): Promise { 25 | params = params || {} 26 | 27 | const activeTextEditor = vscode.window.activeTextEditor 28 | 29 | // Get the workspace folder path 30 | const basePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath 31 | 32 | // Getting language, current_file_name, langcode, and current_file_contents from the active text editor 33 | if (activeTextEditor) { 34 | params.language = activeTextEditor.document.languageId 35 | 36 | if (basePath) { 37 | params.current_file_name = path.relative( 38 | basePath, 39 | activeTextEditor.document.fileName 40 | ) 41 | } 42 | 43 | params.langcode = activeTextEditor.document.languageId // Assuming langcode is same as languageId 44 | params.current_file_contents = activeTextEditor.document.getText() 45 | } 46 | 47 | params.language = params.language || "undefined" 48 | params.current_file_name = params.current_file_name || "undefined" 49 | params.langcode = params.langcode || "undefined" 50 | params.current_file_contents = params.current_file_contents || "undefined" 51 | params.codebase_description = "Unknown codebase." 52 | 53 | const wsFolders = vscode.workspace.workspaceFolders 54 | 55 | if (wsFolders && wsFolders.length > 0) { 56 | // Assume that the first workspace folder is the root of the codebase 57 | const workspaceFolderPath = wsFolders[0].uri.fsPath 58 | 59 | // Load codebase_description from the .spellbound/description.md file 60 | const descriptionFilePath = path.join( 61 | workspaceFolderPath, 62 | ".spellbound", 63 | "description.md" 64 | ) 65 | 66 | try { 67 | params.codebase_description = await fs.readFile( 68 | descriptionFilePath, 69 | "utf-8" 70 | ) 71 | } catch (error) { 72 | vscode.window.showWarningMessage( 73 | "Could not find a .spellbound/description.md file in the workspace folder. Presence of this file improves AI performance." 74 | ) 75 | } 76 | } 77 | 78 | // Read file 79 | const promptFilePath = path.join(__dirname, "..", "assets", "prompt.md") 80 | const sourcePrompt = await fs.readFile(promptFilePath, "utf-8") 81 | 82 | // Fill out the prompt 83 | const filledPrompt = ejs.render(sourcePrompt, { 84 | ...params, 85 | toolList: renderToolList(ToolEngine).join("\n"), 86 | }) 87 | 88 | console.log("prompt", filledPrompt) 89 | 90 | return filledPrompt 91 | } 92 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/AnyToolInterface.ts: -------------------------------------------------------------------------------- 1 | import { CatToolInterface } from "./cat/CatToolInterface" 2 | import { LsToolInterface } from "./ls/LsToolInterface" 3 | import { SearchToolInterface } from "./search/SearchToolInterface" 4 | import { WriteToolInterface } from "./write/WriteToolInterface" 5 | import { AskToolInterface } from "./ask/AskToolInterface" 6 | import { NpmToolInterface } from "./npm/NpmToolInterface" 7 | import { DoneToolInterface } from "./done/DoneToolInterface" 8 | import { StatsToolInterface } from "./stats/StatsToolInterface" 9 | import { GitToolInterface } from "./git/GitToolInterface" 10 | import { GrepToolInterface } from "./grep/GrepToolInterface" 11 | import { MoveToolInterface } from "./move/MoveToolInterface" 12 | 13 | export type AnyToolInterface = 14 | | CatToolInterface 15 | | LsToolInterface 16 | | SearchToolInterface 17 | | WriteToolInterface 18 | | GrepToolInterface 19 | | AskToolInterface 20 | | NpmToolInterface 21 | | GitToolInterface 22 | | DoneToolInterface 23 | | StatsToolInterface 24 | | MoveToolInterface 25 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/ToolEngine.ts: -------------------------------------------------------------------------------- 1 | import { askToolImpl } from "./ask/askToolImpl" 2 | import { catToolImpl } from "./cat/catToolImpl" 3 | import { doneToolImpl } from "./done/doneToolImpl" 4 | import { lsToolImpl } from "./ls/lsToolImpl" 5 | import { npmToolImpl } from "./npm/npmToolImpl" 6 | import { gitToolImpl } from "./git/gitToolImpl" 7 | import { searchToolImpl } from "./search/searchToolImpl" 8 | import { statsToolImpl } from "./stats/statsToolImpl" 9 | import { writeToolImpl } from "./write/writeToolImpl" 10 | import { diffToolImpl } from "./diff/diffToolImpl" 11 | import { grepToolImpl } from "./grep/grepToolImpl" 12 | import { moveToolImpl } from "./move/moveToolImpl" 13 | 14 | 15 | export const ToolEngine = { 16 | ask: [askToolImpl, 17 | "ask {question}", 18 | "Ask the user a question"], 19 | 20 | cat: [catToolImpl, 21 | "cat {path}", 22 | "Read the content of a file at the given path."], 23 | 24 | done: [doneToolImpl, 25 | "done", 26 | "This '## Thought' is the final chain-of-reasoning conclusion."], 27 | 28 | ls: [lsToolImpl, 29 | "ls {regex}", 30 | "List all files in the workspace with path matching the given regex."], 31 | 32 | npm: [npmToolImpl, 33 | "npm {args}", 34 | "Run an npm command (e.g., `npm i some-package`)."], 35 | 36 | git: [gitToolImpl, 37 | "git {args}", 38 | "Run an git command with optional arguments. End your commit messages with '(By SB)'"], 39 | 40 | search: [searchToolImpl, 41 | "search {topic}", 42 | "Search for a topic in the vector-embedding database."], 43 | 44 | stats: [statsToolImpl, 45 | "stats", 46 | "List available vector embedding namespaces"], 47 | 48 | write: [writeToolImpl, 49 | "write {path, contents}", 50 | "Write (or overwrite) the given contents into the specified file. Takes a long time to large."], 51 | 52 | diff: [diffToolImpl, 53 | "diff {source, patchStr}", 54 | "For _modifying_ files. Takes filename and unified diff patch string."], 55 | 56 | grep: [grepToolImpl, 57 | "grep {regex, path?}", 58 | "Perform a grep search with the specified list of glob patterns and a regex query."], 59 | 60 | move: [moveToolImpl, 61 | "move {source, dest}", 62 | "Move a file or folder from source to destination. Paths relative to workspace root."] 63 | 64 | } as const 65 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/ask/AskToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type AskToolInterface = { 2 | tool: "ask" 3 | question: string 4 | } 5 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/ask/askToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { AskToolInterface } from "./AskToolInterface" 2 | 3 | export async function askToolImpl(params: AskToolInterface) { 4 | return undefined 5 | } 6 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/cat/CatToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type CatToolInterface = { 2 | tool: "cat" 3 | path: string 4 | } 5 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/cat/catToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs" 2 | import path from "path" 3 | import * as vscode from "vscode" 4 | import { CatToolInterface } from "./CatToolInterface" 5 | 6 | export async function catToolImpl(params: CatToolInterface): Promise { 7 | const relativeFilePath = params.path 8 | 9 | const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath 10 | 11 | if (!workspacePath) { 12 | return `ERROR: No workspace folder currently open` 13 | } 14 | 15 | const absoluteFilePath = path.join(workspacePath, relativeFilePath) 16 | 17 | try { 18 | const contents = await fs.readFile(absoluteFilePath, "utf8") 19 | return contents 20 | } catch (err) { 21 | return `ERROR: Failed to read file: ${relativeFilePath}` 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/diff/DiffToolInterface.ts: -------------------------------------------------------------------------------- 1 | export interface DiffToolInterface { 2 | source: string; 3 | patchStr: string; 4 | } 5 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/diff/diffToolImpl.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import fs from "fs" 3 | import path from "path" 4 | 5 | import { DiffToolInterface } from "./DiffToolInterface" 6 | import { applyPatch, parsePatch } from "diff" 7 | 8 | export async function diffToolImpl(params: DiffToolInterface): Promise { 9 | const {source, patchStr} = params 10 | 11 | try { 12 | const patches = parsePatch(patchStr) 13 | const workspaceRoot = vscode.workspace.workspaceFolders?.[0].uri.fsPath 14 | const filename = path.join(workspaceRoot || "", source) 15 | const text = fs.readFileSync(filename, "utf8") 16 | const patchedText = applyPatch(text, patches[0]) 17 | 18 | if (!patchedText) { 19 | throw new Error("Failed to apply patch.") 20 | } 21 | 22 | fs.writeFileSync(filename, patchedText) 23 | return "Done." 24 | 25 | } catch (err: any) { 26 | return `ERROR: Failed to apply diff patch: ${err?.message || 'Unknown error'}` 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/done/DoneToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type DoneToolInterface = { 2 | tool: "done" 3 | } 4 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/done/doneToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { DoneToolInterface } from "./DoneToolInterface" 2 | 3 | export async function doneToolImpl(params: DoneToolInterface) { 4 | return undefined 5 | } 6 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/git/GitToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type GitToolInterface = { 2 | tool: "git" 3 | args: string 4 | } 5 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/git/gitToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { GitToolInterface } from "./GitToolInterface" 2 | import * as vscode from "vscode" 3 | import { execSync } from "child_process" 4 | import { runGitCommand } from "./utility/runGitCommand" 5 | 6 | export async function gitToolImpl(params: GitToolInterface): Promise { 7 | const { args } = params 8 | 9 | const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath 10 | 11 | if (!workspacePath) { 12 | return `ERROR: No workspace folder currently open` 13 | } 14 | 15 | try { 16 | return await runGitCommand(args) 17 | } catch (err: any) { 18 | return `ERROR: Failed to execute git command: ${err?.message || 'Unknown error'}` 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/git/utility/runGitCommand.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process" 2 | import * as vscode from "vscode" 3 | 4 | export async function runGitCommand( 5 | args: string, 6 | ): Promise { 7 | return new Promise((resolve, reject) => { 8 | const cwd = vscode.workspace.workspaceFolders?.[0].uri.fsPath 9 | // Execute the npm command in the provided working directory 10 | exec(`git ${args}`, { cwd }, (error, stdout, stderr) => { 11 | if (error) { 12 | const errorMessage = `ERROR: Failed to run git command: ${args}\n${stdout}\n${stderr}` 13 | console.error(errorMessage) 14 | resolve(errorMessage) 15 | } else { 16 | const successMessage = `Successfully ran git command: ${args}\n${stdout}` 17 | resolve(successMessage) 18 | } 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/grep/GrepToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type GrepToolInterface = { 2 | tool: "grep", 3 | regex: string, 4 | path?: string[], 5 | } 6 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/grep/grepToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { GrepToolInterface } from './GrepToolInterface' 2 | import { runGitCommand } from '../git/utility/runGitCommand' 3 | 4 | 5 | type GrepResult = { 6 | line: string 7 | lineNumber: number 8 | } 9 | 10 | export async function grepToolImpl({ regex, path }: GrepToolInterface) { 11 | const result = await runGitCommand(`grep -n --exclude-standard --no-index -e '${regex}' -- ${path || '.'}`) 12 | if (result.startsWith('ERROR')) { 13 | return "No files found." 14 | } 15 | return result 16 | .split('\n') 17 | .slice(1, -1) 18 | .join('\n') || 'No files found' 19 | } 20 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/ls/LsToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type LsToolInterface = { 2 | tool: "ls" 3 | regex: string 4 | } 5 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/ls/lsToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { LsToolInterface } from "./LsToolInterface" 2 | import { runGitCommand } from "../git/utility/runGitCommand" 3 | 4 | export async function lsToolImpl({ regex }: LsToolInterface): Promise { 5 | return (await runGitCommand("ls-files --exclude=node_modules --exclude-standard --deduplicate")) 6 | .split("\n") 7 | .slice(1, -1) 8 | .filter((x) => RegExp(regex).test(x)) 9 | .map((x) => `- ${x}`) 10 | .join("\n") || "No files found" 11 | } 12 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/move/MoveToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type MoveToolInterface = { 2 | tool: "move" 3 | source: string 4 | dest: string 5 | } 6 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/move/moveToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs" 2 | import path from "path" 3 | import * as vscode from "vscode" 4 | import { MoveToolInterface } from "./MoveToolInterface" 5 | 6 | export async function moveToolImpl(params: MoveToolInterface): Promise { 7 | const sourcePath = params.source 8 | const destPath = params.dest 9 | 10 | const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath 11 | 12 | if (!workspacePath) { 13 | return `ERROR: No workspace folder currently open` 14 | } 15 | 16 | const sourceAbsolutePath = path.join(workspacePath, sourcePath) 17 | const destinationAbsolutePath = path.join(workspacePath, destPath) 18 | 19 | try { 20 | await fs.rename(sourceAbsolutePath, destinationAbsolutePath) 21 | return `File moved from ${sourcePath} to ${destPath}` 22 | } catch (err) { 23 | return `ERROR: Failed to move file from ${sourcePath} to ${destPath}` 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/npm/NpmToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type NpmToolInterface = { 2 | tool: "npm" 3 | args: string 4 | } 5 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/npm/npmToolImpl.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import { runNpmCommand } from "./utility/runNpmCommand" 3 | import { NpmToolInterface } from "./NpmToolInterface" 4 | 5 | export async function npmToolImpl(params: NpmToolInterface): Promise { 6 | const { args } = params 7 | 8 | // Base off of workspace directory 9 | const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath 10 | 11 | if (!workspacePath) { 12 | throw new Error("No workspace folder found.") 13 | } 14 | 15 | // Use the helper function to run the npm script 16 | return await runNpmCommand(args) 17 | } 18 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/npm/utility/runNpmCommand.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process" 2 | import * as vscode from "vscode" 3 | 4 | export async function runNpmCommand( 5 | args: string, 6 | ): Promise { 7 | return new Promise((resolve, reject) => { 8 | // Execute the npm command in the provided working directory 9 | exec(`npm ${args}`, { 10 | // workspace root 11 | cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath, 12 | }, (error, stdout, stderr) => { 13 | if (error) { 14 | const errorMessage = `ERROR: Failed to run npm command: ${args}\n${stdout}\n${stderr}` 15 | console.error(errorMessage) 16 | resolve(errorMessage) 17 | } else { 18 | const successMessage = `Successfully ran npm command: ${args}\n${stdout}\n${stderr}` 19 | resolve(successMessage) 20 | } 21 | }) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/search/SearchToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type SearchToolInterface = { 2 | tool: "search" 3 | topic: string 4 | } 5 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/search/searchToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { getEmbedding } from "../../api/openai" 2 | import { query } from "../../api/pinecone" 3 | 4 | import { SearchToolInterface } from "./SearchToolInterface" 5 | import { makeRelative } from "../../utils/paths" 6 | import { ScoredPineconeRecord } from "@pinecone-database/pinecone" 7 | 8 | type Score = { max: number; avg: number } 9 | 10 | const byKey = (scores: Record, key: keyof Score) => { 11 | return Object.keys(scores).sort((a, b) => { 12 | const scoreA = scores[a] 13 | const scoreB = scores[b] 14 | return scoreB[key] - scoreA[key] 15 | }) 16 | } 17 | 18 | const calculateScores = (matches: ScoredPineconeRecord[]) => { 19 | const scores: Record = {} 20 | 21 | for (const result of matches || []) { 22 | const { filename } = result.metadata as any 23 | const score = scores[filename] || { max: 0, avg: 0 } 24 | score.max = Math.max(score.max, result.score || 0) 25 | score.avg += result.score || 0 26 | scores[filename] = score 27 | } 28 | 29 | return scores 30 | } 31 | 32 | export async function searchToolImpl(params: SearchToolInterface) { 33 | const embedding = await getEmbedding(params.topic) 34 | const results = await query(embedding, 5, undefined) 35 | 36 | const scores = calculateScores(results.matches || []) 37 | 38 | const byScore = byKey(scores, "max") 39 | const byAverage = byKey(scores, "avg") 40 | 41 | const topScorePath = makeRelative(byScore[0]) 42 | const topAveragePath = makeRelative(byAverage[0]) 43 | 44 | let response = `Semantic search results: 45 | Top score: ${topScorePath} 46 | Top average: ${topAveragePath}` 47 | 48 | return response 49 | } 50 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/stats/StatsToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type StatsToolInterface = { 2 | tool: "stats" 3 | } 4 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/stats/statsToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { indexStats } from "../../api/pinecone" 2 | 3 | import { StatsToolInterface } from "./StatsToolInterface" 4 | 5 | export async function statsToolImpl(params: StatsToolInterface) { 6 | const stats = await indexStats() 7 | return JSON.stringify(stats, null, 2) 8 | } 9 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/write/WriteToolInterface.ts: -------------------------------------------------------------------------------- 1 | export type WriteToolInterface = { 2 | tool: "write" 3 | path: string 4 | contents: string 5 | } 6 | -------------------------------------------------------------------------------- /apps/spellbound/src/tools/write/writeToolImpl.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs, mkdirSync, existsSync } from "fs" 2 | import path from "path" 3 | import * as vscode from "vscode" 4 | import { WriteToolInterface } from "./WriteToolInterface" 5 | 6 | export async function writeToolImpl( 7 | params: WriteToolInterface 8 | ): Promise { 9 | const filePath = params.path 10 | const contents = params.contents 11 | 12 | // Base off of workspace directory 13 | const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath 14 | 15 | if (!workspacePath) { 16 | return `ERROR: No workspace folder open` 17 | } 18 | 19 | const absoluteFilePath = path.join(workspacePath, filePath) 20 | const fileDirectory = path.dirname(absoluteFilePath) 21 | 22 | if (!existsSync(fileDirectory)) { 23 | mkdirSync(fileDirectory, { recursive: true }) 24 | } 25 | 26 | try { 27 | await fs.writeFile(absoluteFilePath, contents, "utf8") 28 | return `File written to ${filePath}` 29 | } catch (err) { 30 | return `ERROR: Failed to write file: ${filePath}` 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/spellbound/src/utils/chunking.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import path from "path" 3 | import { getCurrentWorkspaceFolder } from "./getCurrentWorkspaceFolder" 4 | 5 | export const IGNORED_EXTENSIONS = [ 6 | ".png", 7 | ".jpg", 8 | ".jpeg", 9 | ".gif", 10 | ".mp3", 11 | ".mp4", 12 | ".mov", 13 | ".avi", 14 | ".zip", 15 | ".gz", 16 | ".tar", 17 | ".tgz", 18 | ".bz2", 19 | ".rar", 20 | ".7z", 21 | ".pdf", 22 | ".doc", 23 | ".docx", 24 | ".xls", 25 | ".xlsx", 26 | ".ppt", 27 | ".pptx", 28 | ".odt", 29 | ".ods", 30 | ] 31 | 32 | export type Chunk = { 33 | filename: string 34 | start: number 35 | end: number 36 | text: string 37 | embedding: number[] | null 38 | } 39 | 40 | export const chunkFile = ( 41 | filename: string, 42 | block_size: number, 43 | block_overlap: number 44 | ) => { 45 | if (IGNORED_EXTENSIONS.includes(path.extname(filename))) { 46 | return [] 47 | } 48 | 49 | console.log("chunking file", filename) 50 | const content = fs.readFileSync(filename, "utf8") 51 | 52 | const chunks: Chunk[] = [] 53 | let start = 0 54 | let end = block_size 55 | let overlap = block_overlap 56 | 57 | while (start < content.length) { 58 | if (end > content.length) { 59 | end = content.length 60 | } 61 | 62 | const relativeFilename = path.relative( 63 | getCurrentWorkspaceFolder(), 64 | filename 65 | ) 66 | 67 | chunks.push({ 68 | filename: relativeFilename, 69 | start, 70 | end, 71 | text: content.substring(start, end), 72 | embedding: null, 73 | }) 74 | 75 | if (end === content.length) { 76 | break 77 | } 78 | 79 | start = end - overlap 80 | end = start + block_size 81 | } 82 | 83 | return chunks 84 | } 85 | 86 | export const chunkFiles = ( 87 | filenames: string[], 88 | block_size: number, 89 | block_overlap: number 90 | ) => { 91 | return filenames.flatMap((filename) => 92 | chunkFile(filename, block_size, block_overlap) 93 | ) 94 | } 95 | 96 | export const chunkFolder = ( 97 | folder: string, 98 | block_size: number, 99 | block_overlap: number 100 | ): Chunk[] => { 101 | const names = fs.readdirSync(folder) 102 | 103 | // ignore hidden files and node_modules 104 | const filteredNames = names 105 | .filter((f) => !f.startsWith(".")) 106 | .filter((f) => f !== "node_modules") 107 | 108 | const fullNames = filteredNames.map((f) => path.join(folder, f)) 109 | 110 | const files = fullNames 111 | .filter((f) => fs.statSync(f).isFile()) 112 | .filter((f) => !IGNORED_EXTENSIONS.includes(path.extname(f))) 113 | 114 | const dirs = fullNames.filter((f) => fs.statSync(f).isDirectory()) 115 | 116 | const chunks = chunkFiles(files, block_size, block_overlap) 117 | const childChunks = dirs.flatMap((d) => 118 | chunkFolder(d, block_size, block_overlap) 119 | ) 120 | 121 | return chunks.concat(childChunks) 122 | } 123 | -------------------------------------------------------------------------------- /apps/spellbound/src/utils/getCurrentWorkspaceFolder.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | 3 | export function getCurrentWorkspaceFolder() { 4 | const workspaceFolder = vscode.workspace.workspaceFolders?.[0].uri.fsPath 5 | 6 | if (!workspaceFolder) { 7 | vscode.window.showWarningMessage("No workspace folder open") 8 | throw new Error("No workspace folder open") 9 | } 10 | 11 | return workspaceFolder 12 | } 13 | -------------------------------------------------------------------------------- /apps/spellbound/src/utils/paths.ts: -------------------------------------------------------------------------------- 1 | import { join, relative } from "path" 2 | import { getCurrentWorkspaceFolder } from "./getCurrentWorkspaceFolder" 3 | 4 | export const makeRelative = (path: string) => 5 | relative(getCurrentWorkspaceFolder(), path) 6 | 7 | export const makeAbsolute = (path: string) => 8 | join(getCurrentWorkspaceFolder(), path) 9 | -------------------------------------------------------------------------------- /apps/spellbound/src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | 2 | export const createUUID = () => { 3 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { 4 | var r = (Math.random() * 16) | 0, 5 | v = c === "x" ? r : (r & 0x3) | 0x8 6 | return v.toString(16) 7 | }) 8 | } -------------------------------------------------------------------------------- /apps/spellbound/src/views/ChatboxViewProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode" 2 | import markdownit from "markdown-it" 3 | import { readFileSync } from "fs" 4 | import { Message, WebviewProcedures, createBirpc } from "spellbound-shared" 5 | import { AgentLogicHandler } from "../agent/AgentLogicHandler" 6 | 7 | export class ChatboxViewProvider implements vscode.WebviewViewProvider { 8 | constructor(private readonly extensionUri: vscode.Uri) { } 9 | 10 | private md = markdownit({ 11 | html: true, 12 | breaks: true, 13 | }) 14 | 15 | messagesToHumanReadable(messages: Message[]): string { 16 | return messages 17 | .map(message => `[${message.role === 'agent' ? 'Agent' : 'User'}]:\n${message.content}`) 18 | .join('\n') 19 | } 20 | 21 | resolveWebviewView(webviewView: vscode.WebviewView) { 22 | webviewView.webview.options = { 23 | enableScripts: true, 24 | localResourceRoots: [this.extensionUri], 25 | } 26 | 27 | webviewView.webview.html = this.getChatboxHtml() 28 | 29 | const agentLogicHandler = new AgentLogicHandler( 30 | createBirpc({ 31 | abort: () => { 32 | agentLogicHandler.abort() 33 | }, 34 | submit: async (messages: Message[]) => { 35 | agentLogicHandler.promptAgent(messages) 36 | }, 37 | saveToFile: async (messages: Message[]) => { 38 | try { 39 | const saveUri = await vscode.window.showSaveDialog({ 40 | filters: { 41 | "Markdown Files": ["md"], 42 | "Text Files": ["txt"] 43 | }, 44 | saveLabel: "Save Chat Messages", 45 | title: "Save Chat Messages As" 46 | }) 47 | 48 | if (saveUri) { 49 | const formattedMessages = this.messagesToHumanReadable(messages) 50 | await vscode.workspace.fs.writeFile(saveUri, Buffer.from(formattedMessages)) 51 | vscode.window.showInformationMessage("Chat messages saved successfully.") 52 | } 53 | } catch (error) { 54 | vscode.window.showErrorMessage(`Error saving file: ${(error as Error).message}`) 55 | } 56 | } 57 | }, { 58 | post: data => { 59 | webviewView.webview.postMessage(data) 60 | }, 61 | on: data => { 62 | webviewView.webview.onDidReceiveMessage(e => { 63 | data(e) 64 | }) 65 | }, 66 | }) 67 | ) 68 | } 69 | 70 | private getChatboxCss() { 71 | const stylesFilename = "static/main.css" 72 | const stylesUri = this.extensionUri.with({ 73 | path: this.extensionUri.path + "/" + stylesFilename, 74 | }) 75 | return readFileSync(stylesUri.fsPath, "utf8") 76 | } 77 | 78 | private getChatboxJs() { 79 | const scriptFilename = "static/main.js" 80 | const scriptUri = this.extensionUri.with({ 81 | path: this.extensionUri.path + "/" + scriptFilename, 82 | }) 83 | return readFileSync(scriptUri.fsPath, "utf8") 84 | } 85 | 86 | private getChatboxHtml() { 87 | const stylesText = this.getChatboxCss() 88 | const scriptText = this.getChatboxJs() 89 | 90 | return /* html */` 91 | 92 | 93 | 94 | 95 | 96 | 97 | Chatbox 98 | 110 | 111 | 112 |
113 | 116 | 117 | 118 | ` 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /apps/spellbound/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": ["ES2020"], 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true 10 | }, 11 | "exclude": ["node_modules", "webview"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/spellbound/webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict' 4 | 5 | const fs = require('fs') 6 | const path = require('path') 7 | const CopyPlugin = require("copy-webpack-plugin") 8 | 9 | // copy ../spellbound-ui/build/static/{js,css} to ./static/ 10 | const uiBuildDir = path.resolve(__dirname, '..', 'spellbound-ui', 'build') 11 | const uiStaticDir = path.resolve(uiBuildDir, 'static') 12 | const uiStaticJsDir = path.resolve(uiStaticDir, 'js') 13 | const uiStaticCssDir = path.resolve(uiStaticDir, 'css') 14 | const uiStaticJsFiles = fs.readdirSync(uiStaticJsDir) 15 | const uiStaticCssFiles = fs.readdirSync(uiStaticCssDir) 16 | 17 | const copyFile = (src, dest) => fs.copyFileSync(src, dest) 18 | const copyFiles = (srcDir, destDir, files) => files.forEach(file => copyFile(path.resolve(srcDir, file), path.resolve(destDir, file))) 19 | 20 | const staticDir = path.resolve(__dirname, 'static') 21 | 22 | if (!fs.existsSync(staticDir)) { 23 | fs.mkdirSync(staticDir) 24 | } 25 | 26 | copyFiles(uiStaticJsDir, staticDir, uiStaticJsFiles) 27 | copyFiles(uiStaticCssDir, staticDir, uiStaticCssFiles) 28 | 29 | 30 | //@ts-check 31 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 32 | 33 | /** @type WebpackConfig */ 34 | const extensionConfig = { 35 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 36 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 37 | 38 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 39 | output: { 40 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 41 | path: path.resolve(__dirname, 'dist'), 42 | filename: 'extension.js', 43 | libraryTarget: 'commonjs2' 44 | }, 45 | externals: { 46 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 47 | // modules added here also need to be added in the .vscodeignore file 48 | }, 49 | resolve: { 50 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 51 | extensions: ['.ts', '.js'] 52 | }, 53 | module: { 54 | rules: [ 55 | { 56 | test: /\.ts$/, 57 | exclude: /node_modules/, 58 | use: [ 59 | { 60 | loader: 'ts-loader' 61 | } 62 | ] 63 | } 64 | ] 65 | }, 66 | plugins: [ 67 | // copy ./static/{js,css} to dist/ 68 | new CopyPlugin({ 69 | patterns: [ 70 | { from: path.resolve(__dirname, 'static/main.js'), to: 'static/main.js' }, 71 | { from: path.resolve(__dirname, 'static/main.css'), to: 'static/main.css' }, 72 | ], 73 | }), 74 | ], 75 | devtool: 'nosources-source-map', 76 | infrastructureLogging: { 77 | level: "log", // enables logging required for problem matchers 78 | }, 79 | } 80 | module.exports = [ extensionConfig ] -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agentitive/spellbound/99c6e70f2aa74322d074d8b88c0b1cdef2d7acdf/assets/logo.png -------------------------------------------------------------------------------- /common/config/rush/.npmrc: -------------------------------------------------------------------------------- 1 | # Rush uses this file to configure the NPM package registry during installation. It is applicable 2 | # to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", 3 | # "rush update", and the "install-run.js" scripts. 4 | # 5 | # NOTE: The "rush publish" command uses .npmrc-publish instead. 6 | # 7 | # Before invoking the package manager, Rush will copy this file to the folder where installation 8 | # is performed. The copied file will omit any config lines that reference environment variables 9 | # that are undefined in that session; this avoids problems that would otherwise result due to 10 | # a missing variable being replaced by an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | # //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | # 21 | registry=https://registry.npmjs.org/ 22 | always-auth=false 23 | -------------------------------------------------------------------------------- /common/config/rush/.npmrc-publish: -------------------------------------------------------------------------------- 1 | # This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish 2 | # is used by the "rush publish" command, as publishing often involves different credentials 3 | # and registries than other operations. 4 | # 5 | # Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" 6 | # and then temporarily map that folder as the "home directory" for the current user account. 7 | # This enables the same settings to apply for each project folder that gets published. The copied file 8 | # will omit any config lines that reference environment variables that are undefined in that session; 9 | # this avoids problems that would otherwise result due to a missing variable being replaced by 10 | # an empty string. 11 | # 12 | # * * * SECURITY WARNING * * * 13 | # 14 | # It is NOT recommended to store authentication tokens in a text file on a lab machine, because 15 | # other unrelated processes may be able to read the file. Also, the file may persist indefinitely, 16 | # for example if the machine loses power. A safer practice is to pass the token via an 17 | # environment variable, which can be referenced from .npmrc using ${} expansion. For example: 18 | # 19 | # //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} 20 | # 21 | -------------------------------------------------------------------------------- /common/config/rush/.pnpmfile.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * When using the PNPM package manager, you can use pnpmfile.js to workaround 5 | * dependencies that have mistakes in their package.json file. (This feature is 6 | * functionally similar to Yarn's "resolutions".) 7 | * 8 | * For details, see the PNPM documentation: 9 | * https://pnpm.js.org/docs/en/hooks.html 10 | * 11 | * IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE 12 | * ANY CACHED DEPENDENCY ANALYSIS. After any modification to pnpmfile.js, it's recommended to run 13 | * "rush update --full" so that PNPM will recalculate all version selections. 14 | */ 15 | module.exports = { 16 | hooks: { 17 | readPackage 18 | } 19 | }; 20 | 21 | /** 22 | * This hook is invoked during installation before a package's dependencies 23 | * are selected. 24 | * The `packageJson` parameter is the deserialized package.json 25 | * contents for the package that is about to be installed. 26 | * The `context` parameter provides a log() function. 27 | * The return value is the updated object. 28 | */ 29 | function readPackage(packageJson, context) { 30 | 31 | // // The karma types have a missing dependency on typings from the log4js package. 32 | // if (packageJson.name === '@types/karma') { 33 | // context.log('Fixed up dependencies for @types/karma'); 34 | // packageJson.dependencies['log4js'] = '0.6.38'; 35 | // } 36 | 37 | return packageJson; 38 | } 39 | -------------------------------------------------------------------------------- /common/config/rush/artifactory.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file manages Rush integration with JFrog Artifactory services. 3 | * More documentation is available on the Rush website: https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/artifactory.schema.json", 7 | 8 | "packageRegistry": { 9 | /** 10 | * (Required) Set this to "true" to enable Rush to manage tokens for an Artifactory NPM registry. 11 | * When enabled, "rush install" will automatically detect when the user's ~/.npmrc 12 | * authentication token is missing or expired. And "rush setup" will prompt the user to 13 | * renew their token. 14 | * 15 | * The default value is false. 16 | */ 17 | "enabled": false, 18 | 19 | /** 20 | * (Required) Specify the URL of your NPM registry. This is the same URL that appears in 21 | * your .npmrc file. It should look something like this example: 22 | * 23 | * https://your-company.jfrog.io/your-project/api/npm/npm-private/ 24 | */ 25 | "registryUrl": "", 26 | 27 | /** 28 | * A list of custom strings that "rush setup" should add to the user's ~/.npmrc file at the time 29 | * when the token is updated. This could be used for example to configure the company registry 30 | * to be used whenever NPM is invoked as a standalone command (but it's not needed for Rush 31 | * operations like "rush add" and "rush install", which get their mappings from the monorepo's 32 | * common/config/rush/.npmrc file). 33 | * 34 | * NOTE: The ~/.npmrc settings are global for the user account on a given machine, so be careful 35 | * about adding settings that may interfere with other work outside the monorepo. 36 | */ 37 | "userNpmrcLinesToAdd": [ 38 | // "@example:registry=https://your-company.jfrog.io/your-project/api/npm/npm-private/" 39 | ], 40 | 41 | /** 42 | * (Required) Specifies the URL of the Artifactory control panel where the user can generate 43 | * an API key. This URL is printed after the "visitWebsite" message. 44 | * It should look something like this example: https://your-company.jfrog.io/ 45 | * Specify an empty string to suppress this line entirely. 46 | */ 47 | "artifactoryWebsiteUrl": "", 48 | 49 | /** 50 | * Uncomment this line to specify the type of credential to save in the user's ~/.npmrc file. 51 | * The default is "password", which means the user's API token will be traded in for an 52 | * npm password specific to that registry. Optionally you can specify "authToken", which 53 | * will save the user's API token as credentials instead. 54 | */ 55 | // "credentialType": "password", 56 | 57 | /** 58 | * These settings allow the "rush setup" interactive prompts to be customized, for 59 | * example with messages specific to your team or configuration. Specify an empty string 60 | * to suppress that message entirely. 61 | */ 62 | "messageOverrides": { 63 | /** 64 | * Overrides the message that normally says: 65 | * "This monorepo consumes packages from an Artifactory private NPM registry." 66 | */ 67 | // "introduction": "", 68 | 69 | /** 70 | * Overrides the message that normally says: 71 | * "Please contact the repository maintainers for help with setting up an Artifactory user account." 72 | */ 73 | // "obtainAnAccount": "", 74 | 75 | /** 76 | * Overrides the message that normally says: 77 | * "Please open this URL in your web browser:" 78 | * 79 | * The "artifactoryWebsiteUrl" string is printed after this message. 80 | */ 81 | // "visitWebsite": "", 82 | 83 | /** 84 | * Overrides the message that normally says: 85 | * "Your user name appears in the upper-right corner of the JFrog website." 86 | */ 87 | // "locateUserName": "", 88 | 89 | /** 90 | * Overrides the message that normally says: 91 | * "Click 'Edit Profile' on the JFrog website. Click the 'Generate API Key' 92 | * button if you haven't already done so previously." 93 | */ 94 | // "locateApiKey": "" 95 | 96 | /** 97 | * Overrides the message that normally prompts: 98 | * "What is your Artifactory user name?" 99 | */ 100 | // "userNamePrompt": "" 101 | 102 | /** 103 | * Overrides the message that normally prompts: 104 | * "What is your Artifactory API key?" 105 | */ 106 | // "apiKeyPrompt": "" 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /common/config/rush/build-cache.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file manages Rush's build cache feature. 3 | * More documentation is available on the Rush website: https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json", 7 | 8 | /** 9 | * (Required) EXPERIMENTAL - Set this to true to enable the build cache feature. 10 | * 11 | * See https://rushjs.io/pages/maintainer/build_cache/ for details about this experimental feature. 12 | */ 13 | "buildCacheEnabled": false, 14 | 15 | /** 16 | * (Required) Choose where project build outputs will be cached. 17 | * 18 | * Possible values: "local-only", "azure-blob-storage", "amazon-s3" 19 | */ 20 | "cacheProvider": "local-only", 21 | 22 | /** 23 | * Setting this property overrides the cache entry ID. If this property is set, it must contain 24 | * a [hash] token. 25 | * 26 | * Other available tokens: 27 | * - [projectName] 28 | * - [projectName:normalize] 29 | * - [phaseName] 30 | * - [phaseName:normalize] 31 | * - [phaseName:trimPrefix] 32 | */ 33 | // "cacheEntryNamePattern": "[projectName:normalize]-[phaseName:normalize]-[hash]" 34 | 35 | /** 36 | * Use this configuration with "cacheProvider"="azure-blob-storage" 37 | */ 38 | "azureBlobStorageConfiguration": { 39 | /** 40 | * (Required) The name of the the Azure storage account to use for build cache. 41 | */ 42 | // "storageAccountName": "example", 43 | 44 | /** 45 | * (Required) The name of the container in the Azure storage account to use for build cache. 46 | */ 47 | // "storageContainerName": "my-container", 48 | 49 | /** 50 | * The Azure environment the storage account exists in. Defaults to AzurePublicCloud. 51 | * 52 | * Possible values: "AzurePublicCloud", "AzureChina", "AzureGermany", "AzureGovernment" 53 | */ 54 | // "azureEnvironment": "AzurePublicCloud", 55 | 56 | /** 57 | * An optional prefix for cache item blob names. 58 | */ 59 | // "blobPrefix": "my-prefix", 60 | 61 | /** 62 | * If set to true, allow writing to the cache. Defaults to false. 63 | */ 64 | // "isCacheWriteAllowed": true 65 | }, 66 | 67 | /** 68 | * Use this configuration with "cacheProvider"="amazon-s3" 69 | */ 70 | "amazonS3Configuration": { 71 | /** 72 | * (Required unless s3Endpoint is specified) The name of the bucket to use for build cache. 73 | * Example: "my-bucket" 74 | */ 75 | // "s3Bucket": "my-bucket", 76 | 77 | /** 78 | * (Required unless s3Bucket is specified) The Amazon S3 endpoint of the bucket to use for build cache. 79 | * This should not include any path; use the s3Prefix to set the path. 80 | * Examples: "my-bucket.s3.us-east-2.amazonaws.com" or "http://localhost:9000" 81 | */ 82 | // "s3Endpoint": "https://my-bucket.s3.us-east-2.amazonaws.com", 83 | 84 | /** 85 | * (Required) The Amazon S3 region of the bucket to use for build cache. 86 | * Example: "us-east-1" 87 | */ 88 | // "s3Region": "us-east-1", 89 | 90 | /** 91 | * An optional prefix ("folder") for cache items. It should not start with "/". 92 | */ 93 | // "s3Prefix": "my-prefix", 94 | 95 | /** 96 | * If set to true, allow writing to the cache. Defaults to false. 97 | */ 98 | // "isCacheWriteAllowed": true 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /common/config/rush/common-versions.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file specifies NPM dependency version selections that affect all projects 3 | * in a Rush repo. More documentation is available on the Rush website: https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", 7 | 8 | /** 9 | * A table that specifies a "preferred version" for a given NPM package. This feature is typically used 10 | * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies. 11 | * 12 | * The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into 13 | * the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager 14 | * will calculate versions. The specific effect depends on your package manager. Generally it will have no 15 | * effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be 16 | * achieved using the pnpmfile.js hook. See the Rush documentation for more details. 17 | * 18 | * After modifying this field, it's recommended to run "rush update --full" so that the package manager 19 | * will recalculate all version selections. 20 | */ 21 | "preferredVersions": { 22 | /** 23 | * When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo, 24 | * instead of the latest version. 25 | */ 26 | // "some-library": "1.2.3" 27 | }, 28 | 29 | /** 30 | * When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions, 31 | * except in cases where different projects specify different version ranges for a given dependency. For older 32 | * package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause 33 | * trouble for indirect dependencies with incompatible peerDependencies ranges. 34 | * 35 | * The default value is true. If you're encountering installation errors related to peer dependencies, 36 | * it's recommended to set this to false. 37 | * 38 | * After modifying this field, it's recommended to run "rush update --full" so that the package manager 39 | * will recalculate all version selections. 40 | */ 41 | // "implicitlyPreferredVersions": false, 42 | 43 | /** 44 | * The "rush check" command can be used to enforce that every project in the repo must specify 45 | * the same SemVer range for a given dependency. However, sometimes exceptions are needed. 46 | * The allowedAlternativeVersions table allows you to list other SemVer ranges that will be 47 | * accepted by "rush check" for a given dependency. 48 | * 49 | * IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE 50 | * USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO). 51 | * This design avoids unnecessary churn in this file. 52 | */ 53 | "allowedAlternativeVersions": { 54 | /** 55 | * For example, allow some projects to use an older TypeScript compiler 56 | * (in addition to whatever "usual" version is being used by other projects in the repo): 57 | */ 58 | // "typescript": [ 59 | // "~2.4.0" 60 | // ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /common/config/rush/experiments.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file allows repo maintainers to enable and disable experimental 3 | * Rush features. More documentation is available on the Rush website: https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", 7 | 8 | /** 9 | * By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'. 10 | * Set this option to true to pass '--frozen-lockfile' instead for faster installs. 11 | */ 12 | // "usePnpmFrozenLockfileForRushInstall": true, 13 | 14 | /** 15 | * By default, 'rush update' passes --no-prefer-frozen-lockfile to 'pnpm install'. 16 | * Set this option to true to pass '--prefer-frozen-lockfile' instead to minimize shrinkwrap changes. 17 | */ 18 | // "usePnpmPreferFrozenLockfileForRushUpdate": true, 19 | 20 | /** 21 | * If using the 'preventManualShrinkwrapChanges' option, restricts the hash to only include the layout of external dependencies. 22 | * Used to allow links between workspace projects or the addition/removal of references to existing dependency versions to not 23 | * cause hash changes. 24 | */ 25 | // "omitImportersFromPreventManualShrinkwrapChanges": true, 26 | 27 | /** 28 | * If true, the chmod field in temporary project tar headers will not be normalized. 29 | * This normalization can help ensure consistent tarball integrity across platforms. 30 | */ 31 | // "noChmodFieldInTarHeaderNormalization": true, 32 | 33 | /** 34 | * If true, build caching will respect the allowWarningsInSuccessfulBuild flag and cache builds with warnings. 35 | * This will not replay warnings from the cached build. 36 | */ 37 | // "buildCacheWithAllowWarningsInSuccessfulBuild": true, 38 | 39 | /** 40 | * If true, the phased commands feature is enabled. To use this feature, create a "phased" command 41 | * in common/config/rush/command-line.json. 42 | */ 43 | // "phasedCommands": true, 44 | 45 | /** 46 | * If true, perform a clean install after when running `rush install` or `rush update` if the 47 | * `.npmrc` file has changed since the last install. 48 | */ 49 | // "cleanInstallAfterNpmrcChanges": true, 50 | 51 | /** 52 | * If true, print the outputs of shell commands defined in event hooks to the console. 53 | */ 54 | // "printEventHooksOutputToConsole": true 55 | } 56 | -------------------------------------------------------------------------------- /common/config/rush/pnpm-config.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file provides settings specific to the PNPM package manager. 3 | * More documentation is available on the Rush website: https://rushjs.io 4 | */ 5 | { 6 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json", 7 | 8 | /** 9 | * If true, then `rush install` and `rush update` will use the PNPM workspaces feature 10 | * to perform the install, instead of the old model where Rush generated the symlinks 11 | * for each projects's node_modules folder. 12 | * 13 | * When using workspaces, Rush will generate a `common/temp/pnpm-workspace.yaml` file referencing 14 | * all local projects to install. Rush will also generate a `.pnpmfile.cjs` shim which implements 15 | * Rush-specific features such as preferred versions. The user's `common/config/rush/.pnpmfile.cjs` 16 | * is invoked by the shim. 17 | * 18 | * This option is strongly recommended. The default value is false. 19 | */ 20 | "useWorkspaces": true, 21 | 22 | /** 23 | * If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when 24 | * invoking PNPM. This causes `rush update` to fail if there are unsatisfied peer dependencies, 25 | * which is an invalid state that can cause build failures or incompatible dependency versions. 26 | * (For historical reasons, JavaScript package managers generally do not treat this invalid 27 | * state as an error.) 28 | * 29 | * PNPM documentation: https://pnpm.io/npmrc#strict-peer-dependencies 30 | * 31 | * The default value is false to avoid legacy compatibility issues. 32 | * It is strongly recommended to set `strictPeerDependencies=true`. 33 | */ 34 | // "strictPeerDependencies": true, 35 | 36 | /** 37 | * Environment variables that will be provided to PNPM. 38 | */ 39 | // "environmentVariables": { 40 | // "NODE_OPTIONS": { 41 | // "value": "--max-old-space-size=4096", 42 | // "override": false 43 | // } 44 | // }, 45 | 46 | /** 47 | * Specifies the location of the PNPM store. There are two possible values: 48 | * 49 | * - `local` - use the `pnpm-store` folder in the current configured temp folder: 50 | * `common/temp/pnpm-store` by default. 51 | * - `global` - use PNPM's global store, which has the benefit of being shared 52 | * across multiple repo folders, but the disadvantage of less isolation for builds 53 | * (for example, bugs or incompatibilities when two repos use different releases of PNPM) 54 | * 55 | * In both cases, the store path can be overridden by the environment variable `RUSH_PNPM_STORE_PATH`. 56 | * 57 | * The default value is `local`. 58 | */ 59 | // "pnpmStore": "global", 60 | 61 | /** 62 | * If true, then `rush install` will report an error if manual modifications 63 | * were made to the PNPM shrinkwrap file without running `rush update` afterwards. 64 | * 65 | * This feature protects against accidental inconsistencies that may be introduced 66 | * if the PNPM shrinkwrap file (`pnpm-lock.yaml`) is manually edited. When this 67 | * feature is enabled, `rush update` will append a hash to the file as a YAML comment, 68 | * and then `rush update` and `rush install` will validate the hash. Note that this 69 | * does not prohibit manual modifications, but merely requires `rush update` be run 70 | * afterwards, ensuring that PNPM can report or repair any potential inconsistencies. 71 | * 72 | * To temporarily disable this validation when invoking `rush install`, use the 73 | * `--bypass-policy` command-line parameter. 74 | * 75 | * The default value is false. 76 | */ 77 | // "preventManualShrinkwrapChanges": true, 78 | 79 | /** 80 | * The "globalOverrides" setting provides a simple mechanism for overriding version selections 81 | * for all dependencies of all projects in the monorepo workspace. The settings are copied 82 | * into the `pnpm.overrides` field of the `common/temp/package.json` file that is generated 83 | * by Rush during installation. 84 | * 85 | * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by 86 | * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, 87 | * and `globalOverrides` has lowest precedence. 88 | * 89 | * PNPM documentation: https://pnpm.io/package_json#pnpmoverrides 90 | */ 91 | "globalOverrides": { 92 | // "example1": "^1.0.0", 93 | // "example2": "npm:@company/example2@^1.0.0" 94 | }, 95 | 96 | /** 97 | * The `globalPeerDependencyRules` setting provides various settings for suppressing validation errors 98 | * that are reported during installation with `strictPeerDependencies=true`. The settings are copied 99 | * into the `pnpm.peerDependencyRules` field of the `common/temp/package.json` file that is generated 100 | * by Rush during installation. 101 | * 102 | * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by 103 | * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, 104 | * and `globalOverrides` has lowest precedence. 105 | * 106 | * https://pnpm.io/package_json#pnpmpeerdependencyrules 107 | */ 108 | "globalPeerDependencyRules": { 109 | // "ignoreMissing": ["@eslint/*"], 110 | // "allowedVersions": { "react": "17" }, 111 | // "allowAny": ["@babel/*"] 112 | }, 113 | 114 | /** 115 | * The `globalPackageExtension` setting provides a way to patch arbitrary package.json fields 116 | * for any PNPM dependency of the monorepo. The settings are copied into the `pnpm.packageExtensions` 117 | * field of the `common/temp/package.json` file that is generated by Rush during installation. 118 | * The `globalPackageExtension` setting has similar capabilities as `.pnpmfile.cjs` but without 119 | * the downsides of an executable script (nondeterminism, unreliable caching, performance concerns). 120 | * 121 | * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by 122 | * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`, 123 | * and `globalOverrides` has lowest precedence. 124 | * 125 | * PNPM documentation: https://pnpm.io/package_json#pnpmpackageextensions 126 | */ 127 | "globalPackageExtensions": { 128 | // "fork-ts-checker-webpack-plugin": { 129 | // "dependencies": { 130 | // "@babel/core": "1" 131 | // }, 132 | // "peerDependencies": { 133 | // "eslint": ">= 6" 134 | // }, 135 | // "peerDependenciesMeta": { 136 | // "eslint": { 137 | // "optional": true 138 | // } 139 | // } 140 | // } 141 | }, 142 | 143 | /** 144 | * The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall` 145 | * lifecycle events for the specified NPM dependencies. This is useful for scripts with poor practices 146 | * such as downloading large binaries without retries or attempting to invoke OS tools such as 147 | * a C++ compiler. (PNPM's terminology refers to these lifecycle events as "building" a package; 148 | * it has nothing to do with build system operations such as `rush build` or `rushx build`.) 149 | * The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json` 150 | * file that is generated by Rush during installation. 151 | * 152 | * PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies 153 | */ 154 | "globalNeverBuiltDependencies": [ 155 | // "fsevents" 156 | ], 157 | 158 | /** 159 | * The `globalAllowedDeprecatedVersions` setting suppresses installation warnings for package 160 | * versions that the NPM registry reports as being deprecated. This is useful if the 161 | * deprecated package is an indirect dependency of an external package that has not released a fix. 162 | * The settings are copied into the `pnpm.allowedDeprecatedVersions` field of the `common/temp/package.json` 163 | * file that is generated by Rush during installation. 164 | * 165 | * PNPM documentation: https://pnpm.io/package_json#pnpmalloweddeprecatedversions 166 | * 167 | * If you are working to eliminate a deprecated version, it's better to specify `allowedDeprecatedVersions` 168 | * in the package.json file for individual Rush projects. 169 | */ 170 | "globalAllowedDeprecatedVersions": { 171 | // "request": "*" 172 | }, 173 | 174 | 175 | /** 176 | * (THIS FIELD IS MACHINE GENERATED) The "globalPatchedDependencies" field is updated automatically 177 | * by the `rush-pnpm patch-commit` command. It is a dictionary, where the key is an NPM package name 178 | * and exact version, and the value is a relative path to the associated patch file. 179 | * 180 | * PNPM documentation: https://pnpm.io/package_json#pnpmpatcheddependencies 181 | */ 182 | "globalPatchedDependencies": { }, 183 | 184 | /** 185 | * (USE AT YOUR OWN RISK) This is a free-form property bag that will be copied into 186 | * the `common/temp/package.json` file that is generated by Rush during installation. 187 | * This provides a way to experiment with new PNPM features. These settings will override 188 | * any other Rush configuration associated with a given JSON field except for `.pnpmfile.cjs`. 189 | * 190 | * USAGE OF THIS SETTING IS NOT SUPPORTED BY THE RUSH MAINTAINERS AND MAY CAUSE RUSH 191 | * TO MALFUNCTION. If you encounter a missing PNPM setting that you believe should 192 | * be supported, please create a GitHub issue or PR. Note that Rush does not aim to 193 | * support every possible PNPM setting, but rather to promote a battle-tested installation 194 | * strategy that is known to provide a good experience for large teams with lots of projects. 195 | */ 196 | "unsupportedPackageJsonSettings": { 197 | // "dependencies": { 198 | // "not-a-good-practice": "*" 199 | // }, 200 | // "scripts": { 201 | // "do-something": "echo Also not a good practice" 202 | // }, 203 | // "pnpm": { "futurePnpmFeature": true } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /common/config/rush/repo-state.json: -------------------------------------------------------------------------------- 1 | // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. 2 | { 3 | "preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" 4 | } 5 | -------------------------------------------------------------------------------- /common/config/rush/rush-plugins.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This configuration file manages Rush's plugin feature. 3 | */ 4 | { 5 | "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json", 6 | "plugins": [ 7 | /** 8 | * Each item configures a plugin to be loaded by Rush. 9 | */ 10 | // { 11 | // /** 12 | // * The name of the NPM package that provides the plugin. 13 | // */ 14 | // "packageName": "@scope/my-rush-plugin", 15 | // /** 16 | // * The name of the plugin. This can be found in the "pluginName" 17 | // * field of the "rush-plugin-manifest.json" file in the NPM package folder. 18 | // */ 19 | // "pluginName": "my-plugin-name", 20 | // /** 21 | // * The name of a Rush autoinstaller that will be used for installation, which 22 | // * can be created using "rush init-autoinstaller". Add the plugin's NPM package 23 | // * to the package.json "dependencies" of your autoinstaller, then run 24 | // * "rush update-autoinstaller". 25 | // */ 26 | // "autoinstallerName": "rush-plugins" 27 | // } 28 | ] 29 | } -------------------------------------------------------------------------------- /common/config/rush/version-policies.json: -------------------------------------------------------------------------------- 1 | /** 2 | * This is configuration file is used for advanced publishing configurations with Rush. 3 | * More documentation is available on the Rush website: https://rushjs.io 4 | */ 5 | 6 | /** 7 | * A list of version policy definitions. A "version policy" is a custom package versioning 8 | * strategy that affects "rush change", "rush version", and "rush publish". The strategy applies 9 | * to a set of projects that are specified using the "versionPolicyName" field in rush.json. 10 | */ 11 | [ 12 | // { 13 | // /** 14 | // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). 15 | // * 16 | // * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This 17 | // * strategy is appropriate for a set of packages that act as selectable components of a 18 | // * unified product. The entire set of packages are always published together, and always share 19 | // * the same NPM version number. When the packages depend on other packages in the set, the 20 | // * SemVer range is usually restricted to a single version. 21 | // */ 22 | // "definitionName": "lockStepVersion", 23 | // 24 | // /** 25 | // * (Required) The name that will be used for the "versionPolicyName" field in rush.json. 26 | // * This name is also used command-line parameters such as "--version-policy" 27 | // * and "--to-version-policy". 28 | // */ 29 | // "policyName": "MyBigFramework", 30 | // 31 | // /** 32 | // * (Required) The current version. All packages belonging to the set should have this version 33 | // * in the current branch. When bumping versions, Rush uses this to determine the next version. 34 | // * (The "version" field in package.json is NOT considered.) 35 | // */ 36 | // "version": "1.0.0", 37 | // 38 | // /** 39 | // * (Required) The type of bump that will be performed when publishing the next release. 40 | // * When creating a release branch in Git, this field should be updated according to the 41 | // * type of release. 42 | // * 43 | // * Valid values are: "prerelease", "release", "minor", "patch", "major" 44 | // */ 45 | // "nextBump": "prerelease", 46 | // 47 | // /** 48 | // * (Optional) If specified, all packages in the set share a common CHANGELOG.md file. 49 | // * This file is stored with the specified "main" project, which must be a member of the set. 50 | // * 51 | // * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each 52 | // * package in the set. 53 | // */ 54 | // "mainProject": "my-app", 55 | // 56 | // /** 57 | // * (Optional) If enabled, the "rush change" command will prompt the user for their email address 58 | // * and include it in the JSON change files. If an organization maintains multiple repos, tracking 59 | // * this contact information may be useful for a service that automatically upgrades packages and 60 | // * needs to notify engineers whose change may be responsible for a downstream build break. It might 61 | // * also be useful for crediting contributors. Rush itself does not do anything with the collected 62 | // * email addresses. The default value is "false". 63 | // */ 64 | // // "includeEmailInChangeFile": true 65 | // }, 66 | // 67 | // { 68 | // /** 69 | // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). 70 | // * 71 | // * The "individualVersion" mode specifies that the projects will use "individual versioning". 72 | // * This is the typical NPM model where each package has an independent version number 73 | // * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the 74 | // * packages, they otherwise don't have any special relationship. The version bumping will 75 | // * depend on how developers answer the "rush change" questions for each package that 76 | // * is changed. 77 | // */ 78 | // "definitionName": "individualVersion", 79 | // 80 | // "policyName": "MyRandomLibraries", 81 | // 82 | // /** 83 | // * (Optional) This can be used to enforce that all packages in the set must share a common 84 | // * major version number, e.g. because they are from the same major release branch. 85 | // * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes 86 | // * inappropriately. The minor/patch version parts will be bumped independently according 87 | // * to the types of changes made to each project, according to the "rush change" command. 88 | // */ 89 | // "lockedMajor": 3, 90 | // 91 | // /** 92 | // * (Optional) When publishing is managed by Rush, by default the "rush change" command will 93 | // * request changes for any projects that are modified by a pull request. These change entries 94 | // * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates 95 | // * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects 96 | // * belonging to this version policy. 97 | // */ 98 | // "exemptFromRushChange": false, 99 | // 100 | // // "includeEmailInChangeFile": true 101 | // } 102 | ] 103 | -------------------------------------------------------------------------------- /common/git-hooks/commit-msg.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is an example Git hook for use with Rush. To enable this hook, rename this file 4 | # to "commit-msg" and then run "rush install", which will copy it from common/git-hooks 5 | # to the .git/hooks folder. 6 | # 7 | # TO LEARN MORE ABOUT GIT HOOKS 8 | # 9 | # The Git documentation is here: https://git-scm.com/docs/githooks 10 | # Some helpful resources: https://githooks.com 11 | # 12 | # ABOUT THIS EXAMPLE 13 | # 14 | # The commit-msg hook is called by "git commit" with one argument, the name of the file 15 | # that has the commit message. The hook should exit with non-zero status after issuing 16 | # an appropriate message if it wants to stop the commit. The hook is allowed to edit 17 | # the commit message file. 18 | 19 | # This example enforces that commit message should contain a minimum amount of 20 | # description text. 21 | if [ `cat $1 | wc -w` -lt 3 ]; then 22 | echo "" 23 | echo "Invalid commit message: The message must contain at least 3 words." 24 | exit 1 25 | fi 26 | -------------------------------------------------------------------------------- /common/scripts/install-run-rush-pnpm.js: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 2 | // 3 | // This script is intended for usage in an automated build environment where the Rush command may not have 4 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 5 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to the 6 | // rush-pnpm command. 7 | // 8 | // An example usage would be: 9 | // 10 | // node common/scripts/install-run-rush-pnpm.js pnpm-command 11 | // 12 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 13 | 14 | /******/ (() => { // webpackBootstrap 15 | /******/ "use strict"; 16 | var __webpack_exports__ = {}; 17 | /*!*****************************************************!*\ 18 | !*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***! 19 | \*****************************************************/ 20 | 21 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 22 | // See the @microsoft/rush package's LICENSE file for license information. 23 | require('./install-run-rush'); 24 | //# sourceMappingURL=install-run-rush-pnpm.js.map 25 | module.exports = __webpack_exports__; 26 | /******/ })() 27 | ; 28 | //# sourceMappingURL=install-run-rush-pnpm.js.map -------------------------------------------------------------------------------- /common/scripts/install-run-rushx.js: -------------------------------------------------------------------------------- 1 | // THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. 2 | // 3 | // This script is intended for usage in an automated build environment where the Rush command may not have 4 | // been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush 5 | // specified in the rush.json configuration file (if not already installed), and then pass a command-line to the 6 | // rushx command. 7 | // 8 | // An example usage would be: 9 | // 10 | // node common/scripts/install-run-rushx.js custom-command 11 | // 12 | // For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ 13 | 14 | /******/ (() => { // webpackBootstrap 15 | /******/ "use strict"; 16 | var __webpack_exports__ = {}; 17 | /*!*************************************************!*\ 18 | !*** ./lib-esnext/scripts/install-run-rushx.js ***! 19 | \*************************************************/ 20 | 21 | // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. 22 | // See the @microsoft/rush package's LICENSE file for license information. 23 | require('./install-run-rush'); 24 | //# sourceMappingURL=install-run-rushx.js.map 25 | module.exports = __webpack_exports__; 26 | /******/ })() 27 | ; 28 | //# sourceMappingURL=install-run-rushx.js.map -------------------------------------------------------------------------------- /rigs/eslint-plugin-local/README.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-local 2 | 3 | Linting configuration for solid projects 4 | 5 | ## Installation 6 | 7 | You'll first need to install [ESLint](https://eslint.org/): 8 | 9 | ```sh 10 | npm i eslint --save-dev 11 | ``` 12 | 13 | Next, install `eslint-plugin-local`: 14 | 15 | ```sh 16 | npm install eslint-plugin-local --save-dev 17 | ``` 18 | 19 | ## Usage 20 | 21 | Add `local` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: 22 | 23 | ```json 24 | { 25 | "plugins": [ 26 | "local" 27 | ] 28 | } 29 | ``` 30 | 31 | 32 | -------------------------------------------------------------------------------- /rigs/eslint-plugin-local/lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Linting configuration for solid projects 3 | * @author Dustin 4 | */ 5 | "use strict" 6 | 7 | //------------------------------------------------------------------------------ 8 | // Requirements 9 | //------------------------------------------------------------------------------ 10 | 11 | const requireIndex = require("requireindex") 12 | const path = require("path") 13 | //------------------------------------------------------------------------------ 14 | // Plugin Definition 15 | //------------------------------------------------------------------------------ 16 | 17 | module.exports = { 18 | configs: { 19 | node: { 20 | ignorePatterns: [ 21 | "node_modules", 22 | "dist", 23 | "docs", 24 | "deploy", 25 | ], 26 | extends: [ 27 | "eslint:recommended", 28 | "plugin:@typescript-eslint/recommended" 29 | ], 30 | parser: "@typescript-eslint/parser", 31 | parserOptions: { 32 | project: ["./tsconfig.json"], 33 | sourceType: "module", 34 | ecmaFeatures: { 35 | modules: true, 36 | }, 37 | extraFileExtensions: [".md", ".json"], 38 | }, 39 | env: { 40 | node: true, 41 | es6: true, 42 | }, 43 | plugins: [ 44 | "@typescript-eslint", 45 | "import", 46 | "import-newlines", 47 | "simple-import-sort", 48 | "unused-imports", 49 | "decorator-position", 50 | ], 51 | rules: { 52 | indent: ["error", 4, { SwitchCase: 1 }], 53 | semi: ["error", "never"], 54 | camelcase: "off", 55 | "no-constant-condition": "off", 56 | "no-unused-vars": "off", 57 | "no-invalid-this": "off", 58 | "no-useless-escape": "off", 59 | "no-useless-constructor": "off", 60 | 61 | "@typescript-eslint/no-floating-promises": "error", 62 | "@typescript-eslint/no-explicit-any": "off", 63 | "@typescript-eslint/ban-types": "off", 64 | "@typescript-eslint/no-empty-function": "off", 65 | "@typescript-eslint/no-var-requires": "off", 66 | "@typescript-eslint/explicit-member-accessibility": [ 67 | "error", 68 | { 69 | overrides: { 70 | accessors: "off", 71 | constructors: "no-public", 72 | methods: "off", 73 | properties: "explicit", 74 | parameterProperties: "off", 75 | }, 76 | }, 77 | ], 78 | "@typescript-eslint/no-invalid-this": ["error"], 79 | "@typescript-eslint/no-shadow": ["warn"], 80 | "@typescript-eslint/interface-name-prefix": "off", 81 | "@typescript-eslint/array-type": [ 82 | "error", 83 | { default: "array-simple" }, 84 | ], 85 | "@typescript-eslint/no-use-before-define": "off", 86 | "@typescript-eslint/explicit-member-accessibility": "off", 87 | "@typescript-eslint/explicit-function-return-type": "off", 88 | "@typescript-eslint/no-non-null-assertion": "off", 89 | "@typescript-eslint/no-unused-vars": "error", 90 | "@typescript-eslint/explicit-module-boundary-types": "off", 91 | 92 | "unused-imports/no-unused-imports": "error", 93 | "import/no-duplicates": "error", 94 | "import/first": "error", 95 | "import/newline-after-import": [ 96 | "error", 97 | { 98 | count: 2, 99 | }, 100 | ], 101 | "import-newlines/enforce": [ 102 | "error", 103 | { 104 | semi: false, 105 | maxLen: 80, 106 | items: 4, 107 | forceSingleLine: false, 108 | }, 109 | ], 110 | "simple-import-sort/imports": "error", 111 | "simple-import-sort/exports": "error", 112 | 113 | "decorator-position/decorator-position": [ 114 | "error", 115 | { 116 | properties: "above", 117 | }, 118 | ], 119 | }, 120 | overrides: [ 121 | { 122 | files: ["src/**/*.spec.ts"], 123 | env: { 124 | jest: true, 125 | node: true 126 | }, 127 | parserOptions: { 128 | project: ["tsconfig.json"], 129 | sourceType: "module", 130 | ecmaFeatures: { 131 | modules: true, 132 | }, 133 | extraFileExtensions: [".md", ".json"], 134 | }, 135 | }, 136 | ], 137 | }, 138 | }, 139 | } 140 | -------------------------------------------------------------------------------- /rigs/eslint-plugin-local/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-local", 3 | "version": "0.0.1", 4 | "main": "./lib/index.js", 5 | "exports": "./lib/index.js", 6 | "scripts": { 7 | "build": "", 8 | "lint": "eslint ." 9 | }, 10 | "dependencies": { 11 | "@typescript-eslint/eslint-plugin": "^5.56.0", 12 | "@typescript-eslint/parser": "^5.56.0", 13 | "requireindex": "^1.2.0" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^8.36.0", 17 | "eslint-plugin-eslint-plugin": "^5.0.0", 18 | "eslint-plugin-node": "^11.1.0", 19 | "eslint-plugin-decorator-position": "^5.0.1", 20 | "eslint-plugin-import": "^2.26.0", 21 | "eslint-plugin-import-newlines": "^1.2.3", 22 | "eslint-plugin-simple-import-sort": "^8.0.0", 23 | "eslint-plugin-unused-imports": "^2.0.0", 24 | "mocha": "^10.0.0" 25 | }, 26 | "engines": { 27 | "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" 28 | }, 29 | "peerDependencies": { 30 | "eslint": ">=7" 31 | }, 32 | "license": "ISC" 33 | } 34 | -------------------------------------------------------------------------------- /rigs/node-rig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-rig", 3 | "version": "0.0.1", 4 | "description": "A rig package for Node.js projects that build using Heft", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "", 8 | "_phase:build": "" 9 | }, 10 | "peerDependencies": { 11 | "@rushstack/heft": "^0.48.7" 12 | }, 13 | "dependencies": { 14 | "eslint": "^8.36.0", 15 | "jest-environment-node": "~27.4.2", 16 | "typescript": "~5.0.3" 17 | }, 18 | "devDependencies": { 19 | "@rushstack/heft": "0.48.7", 20 | "eslint": "^8.36.0", 21 | "typescript": "~5.0.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rigs/node-rig/profiles/default/tsconfig-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "removeComments": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "typeRoots": [ 15 | "../../../../node_modules/@types", 16 | ], 17 | "pretty": true, 18 | "stripInternal": true, 19 | "preserveSymlinks": true, 20 | "outDir": "../../../../dist", 21 | "sourceRoot": "../../../../src", 22 | "incremental": true, 23 | "resolveJsonModule": true, 24 | "types": [ 25 | "node" 26 | ], 27 | "experimentalDecorators": true, 28 | "emitDecoratorMetadata": true, 29 | "skipLibCheck": true, 30 | "lib": ["ES2021.WeakRef"] 31 | }, 32 | "include": [ 33 | "../../../../src", 34 | ], 35 | "exclude": [ 36 | "../../../../node_modules" 37 | ] 38 | } -------------------------------------------------------------------------------- /rigs/repo-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "repo-scripts", 3 | "version": "0.0.1", 4 | "author": "John Chung", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "", 9 | "generate-snippets": "ts-node src/generateSnippets.ts", 10 | "audit": "ts-node src/audit.ts", 11 | "versions": "ts-node src/versions.ts" 12 | }, 13 | "dependencies": { 14 | "@microsoft/rush-lib": "5.82.1", 15 | "source-map-support": "^0.5.20", 16 | "colors": "^1.4.0", 17 | "table": "^6.8.1" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "~18.15.11", 21 | "@types/source-map-support": "^0.5.4", 22 | "@typescript-eslint/eslint-plugin": "^5.56.0", 23 | "@typescript-eslint/parser": "^5.56.0", 24 | "types-package-json": "^2.0.39", 25 | "node-rig": "workspace:^0.0.1", 26 | "eslint-plugin-local": "workspace:^0.0.1", 27 | "eslint": "^8.36.0", 28 | "eslint-plugin-import": "^2.26.0", 29 | "eslint-plugin-import-newlines": "^1.2.3", 30 | "eslint-plugin-json": "^3.1.0", 31 | "eslint-plugin-promise": "^6.0.1", 32 | "eslint-plugin-simple-import-sort": "^8.0.0", 33 | "eslint-plugin-unused-imports": "^2.0.0", 34 | "prettier": "^2.7.1", 35 | "ts-node": "^10.9.1", 36 | "ts-node-dev": "^2.0.0", 37 | "typescript": "~5.0.3" 38 | }, 39 | "engines": { 40 | "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" 41 | }, 42 | "peerDependencies": { 43 | "eslint": ">=7" 44 | }, 45 | "license": "ISC" 46 | } 47 | -------------------------------------------------------------------------------- /rigs/repo-scripts/src/audit.ts: -------------------------------------------------------------------------------- 1 | import { green, red, yellow } from "colors"; 2 | import { table } from "table"; 3 | import { getRemotePackageDependencies } from "./utils"; 4 | import { allLibraries } from "./utils"; 5 | 6 | type PackageInfo = { 7 | name: string; 8 | published: boolean; 9 | problems: string[]; 10 | } 11 | 12 | async function checkPackage(packageName: string) { 13 | const dependencies = await getRemotePackageDependencies(packageName); 14 | 15 | if (!dependencies) { 16 | return { 17 | name: packageName, 18 | published: false, 19 | problems: [] 20 | } 21 | } 22 | 23 | const problems = Object.entries(dependencies) 24 | .filter(([depName]) => depName.startsWith("workspace:")) 25 | .map(([depName]) => depName) 26 | 27 | return { 28 | name: packageName, 29 | published: true, 30 | problems 31 | } 32 | } 33 | 34 | function sortPackages(packages: PackageInfo[]) { 35 | return packages.sort((a, b) => { 36 | if (a.published && !b.published) { 37 | return -1; 38 | } 39 | if (!a.published && b.published) { 40 | return 1; 41 | } 42 | return a.name.localeCompare(b.name); 43 | }); 44 | } 45 | 46 | function formatPackage(packageInfo: PackageInfo) { 47 | const { name, published, problems } = packageInfo; 48 | const title = problems.length ? red(name) : published ? green(name) : yellow(name); 49 | return [title, published ? problems.join(", ") : "Not published."] 50 | } 51 | 52 | async function main() { 53 | const projects = allLibraries() 54 | const infos = await Promise.all(projects.map(p => checkPackage(p.packageName))); 55 | const sorted = sortPackages(infos); 56 | const rows = sorted.map(formatPackage); 57 | console.log("Workspace dependency problems:") 58 | console.log(table([ 59 | ["Package", "Problems"], 60 | ...rows 61 | ])); 62 | } 63 | 64 | main(); -------------------------------------------------------------------------------- /rigs/repo-scripts/src/bumpVersions.ts: -------------------------------------------------------------------------------- 1 | import { RushConfigurationProject } from "@microsoft/rush-lib" 2 | import { readFileSync, writeFileSync } from "fs" 3 | import { allApps, allLibraries } from "./utils" 4 | import type { PackageJson } from 'types-package-json'; 5 | import { execSync } from "child_process" 6 | 7 | 8 | const depTypes: ((keyof PackageJson) & ('dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies'))[] = [ 9 | "dependencies", 10 | "devDependencies", 11 | "peerDependencies", 12 | "optionalDependencies" 13 | ] 14 | 15 | function readPackageJson(lib: RushConfigurationProject) { 16 | const file = readFileSync(`${lib.publishFolder}/package.json`, "utf8") 17 | const packageJson = JSON.parse(file) as PackageJson 18 | return packageJson 19 | } 20 | 21 | function bumpDeps(libs: string[], versionNumber: string, packageJson: PackageJson, depType: keyof PackageJson & ('dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies')) { 22 | if (!packageJson[depType]) { 23 | return 24 | } 25 | const deps = packageJson[depType] 26 | const libDeps = Object.keys(deps!).filter((dep) => libs.includes(dep)) 27 | // Bump versions of the deps 28 | libDeps.forEach((dep) => { 29 | deps![dep] = `workspace:^${versionNumber}` 30 | }) 31 | } 32 | 33 | function commitChanges(msg: string) { 34 | return `git add --all && git commit -m "${msg}"` 35 | } 36 | 37 | function bumpLibVersion(libPath: string, versionNumber: string) { 38 | return `cd libs/${libPath} && pnpm version ${versionNumber}` 39 | } 40 | 41 | function getAllLibsAndApps(libs: RushConfigurationProject[], apps: RushConfigurationProject[]) { 42 | return [...libs, ...apps] 43 | } 44 | 45 | function getPackageNames(libs: RushConfigurationProject[]) { 46 | return libs.map((lib) => readPackageJson(lib).name) 47 | } 48 | 49 | function getAllPackageNames(libs: RushConfigurationProject[], apps: RushConfigurationProject[]) { 50 | const allLibNames = getPackageNames(libs) 51 | const allAppNames = getPackageNames(apps) 52 | 53 | return [...allLibNames, ...allAppNames] 54 | } 55 | 56 | function run() { 57 | const versionNumber = process.argv[3] 58 | const libs = allLibraries() 59 | const apps = allApps() 60 | 61 | const libsAndApps = getAllLibsAndApps(libs, apps) 62 | const allPackageNames = getAllPackageNames(libs, apps) 63 | 64 | // Bump all solid deps to the new version in each solid lib 65 | for (const lib of libsAndApps) { 66 | 67 | const packageJson = readPackageJson(lib) 68 | 69 | depTypes.forEach((depType) => bumpDeps(allPackageNames, versionNumber, packageJson, depType)) 70 | writeFileSync(`${lib.publishFolder}/package.json`, JSON.stringify(packageJson, null, 4)) 71 | } 72 | 73 | execSync( 74 | commitChanges(`Bump all solid deps to ${versionNumber}`), 75 | { 76 | stdio: "inherit" 77 | } 78 | ) 79 | // Bump lib versions 80 | for (const lib of libs) { 81 | execSync( 82 | `${bumpLibVersion(lib.unscopedTempProjectName, versionNumber)} && 83 | cd ../../`, 84 | { 85 | stdio: "inherit" 86 | } 87 | ) 88 | } 89 | 90 | execSync( 91 | commitChanges(`Bump all libs to ${versionNumber}`), 92 | { 93 | stdio: "inherit" 94 | } 95 | ) 96 | } 97 | 98 | run() -------------------------------------------------------------------------------- /rigs/repo-scripts/src/generateSnippets.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { parse, resolve } from "path"; 3 | 4 | export function generateSnippets() { 5 | const snippetsFiles = readSnippetsDirectory() 6 | const snippetDefinitions = generateSnippetDefinitions(snippetsFiles) 7 | const snippetsContents = buildSnippetsContents(snippetDefinitions) 8 | const snippetsFilepath = getSnippetsFilepath() 9 | fs.writeFileSync(snippetsFilepath, snippetsContents); 10 | } 11 | 12 | function readSnippetsDirectory() { 13 | const snippetsDirectory = resolve("../../", ".vscode/snippets") 14 | const snippetsFiles = fs.readdirSync(snippetsDirectory) 15 | return snippetsFiles 16 | } 17 | 18 | function generateSnippetDefinitions(snippetsFiles: string[]): Record { 19 | return snippetsFiles.reduce(parseSnippet, {}) as Record 20 | } 21 | 22 | function parseSnippet(allSnippetDefinitions: {}, filename: string) { 23 | const [prefix, description, ...lines] = readAndParseSnippetFile(filename) 24 | 25 | const cleanedPrefix = extractPrefix(prefix) 26 | const cleanedDescription = extractDescription(description) 27 | const snippetTemplateBody = generateSnippetTemplateBody(cleanedPrefix, cleanedDescription, lines); 28 | const snippetName = parse(filename).name; 29 | 30 | return { ...allSnippetDefinitions, [snippetName]: snippetTemplateBody }; 31 | } 32 | 33 | function readAndParseSnippetFile(filename: string) { 34 | const snippetText = fs.readFileSync(`../../.vscode/snippets/${filename}`, "utf8"); 35 | const [prefix, description, ...lines] = snippetText.split("\n"); 36 | return [prefix, description, ...lines] 37 | } 38 | 39 | function extractPrefix(prefix: string) { 40 | return prefix.replace(/^(\/\/\s*)?prefix\:\s*/, "") 41 | } 42 | 43 | function extractDescription(description: string) { 44 | return description.replace(/^(\/\/\s*)?description\:\s*/, "") 45 | } 46 | 47 | function generateSnippetTemplateBody(prefix: string, description: string, body: string[]) { 48 | return { 49 | scope: "javascript,typescript", 50 | prefix, 51 | body, 52 | description, 53 | }; 54 | } 55 | 56 | function buildSnippetsContents(snippetDefinitions: Record) { 57 | return JSON.stringify(snippetDefinitions, null, 2); 58 | } 59 | 60 | function getSnippetsFilepath() { 61 | return resolve("../../", ".vscode/solid-monorepo.code-snippets") 62 | } 63 | 64 | generateSnippets() -------------------------------------------------------------------------------- /rigs/repo-scripts/src/publishLibrary.ts: -------------------------------------------------------------------------------- 1 | import { allLibraries } from "./utils" 2 | import { execSync } from "child_process" 3 | 4 | 5 | function getPackagePath(packageName: string) { 6 | const library = allLibraries().find((lib) => lib.packageName === packageName) 7 | 8 | if (!library) { 9 | throw new Error(`Could not find library named: ${packageName}`) 10 | } 11 | 12 | return library.publishFolder 13 | 14 | } 15 | 16 | function saveBackupTsconfig(libraryPath: string) { 17 | return `cp ${libraryPath}/tsconfig.json ~/tmpfile` 18 | } 19 | 20 | function useTsconfigTemplate(libraryPath: string) { 21 | return `cp rigs/repo-scripts/templates/tsconfig.json ${libraryPath}/tsconfig.json` 22 | } 23 | 24 | function addPublishArtifact(libraryPath: string) { 25 | return `cd ${libraryPath} && git add --all && git commit -m "Add publish artifact"` 26 | } 27 | 28 | function executePublish() { 29 | return `pnpm publish` 30 | } 31 | 32 | function removePublishArtifact() { 33 | return `mv ~/tmpfile tsconfig.json && git add --all && git commit -m "Remove publish artifact" && git push` 34 | } 35 | 36 | function publishPackage(libraryPath: string) { 37 | execSync( 38 | `${saveBackupTsconfig(libraryPath)} && 39 | ${useTsconfigTemplate(libraryPath)} && 40 | ${addPublishArtifact(libraryPath)} && 41 | ${executePublish()} && 42 | ${removePublishArtifact()}`, 43 | { 44 | stdio: "inherit" 45 | } 46 | ) 47 | } 48 | 49 | function run() { 50 | const packagePath = getPackagePath(`@volley/${process.argv[3]}`) 51 | publishPackage(packagePath) 52 | } 53 | 54 | run() -------------------------------------------------------------------------------- /rigs/repo-scripts/src/publishLibs.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process" 2 | import { allLibraries } from "./utils" 3 | 4 | 5 | function executePublish(libPath: string) { 6 | return `cd libs/${libPath} && pnpm publish` 7 | } 8 | 9 | function run() { 10 | const libs = allLibraries() 11 | 12 | for (const lib of libs) { 13 | execSync( 14 | `${executePublish(lib.unscopedTempProjectName)} && 15 | cd ../../`, 16 | { 17 | stdio: "inherit" 18 | } 19 | ) 20 | } 21 | } 22 | 23 | run() -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './npm'; 2 | export * from './reflect'; 3 | -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/npm/PackageMetadata.ts: -------------------------------------------------------------------------------- 1 | export type PackageMetadata = { 2 | _id: string 3 | _rev: string 4 | name: string 5 | description: string 6 | "dist-tags": { 7 | latest: string 8 | } & Record 9 | version: string 10 | versions: Record 11 | dependencies: Record 12 | peerDependencies: Record 13 | devDependencies: Record 14 | scripts: Record 15 | } -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/npm/getRemotePackageDependencies.ts: -------------------------------------------------------------------------------- 1 | import { getRemotePackageMetadata } from "./getRemotePackageMetadata"; 2 | 3 | 4 | export async function getRemotePackageDependencies(packageName: string) { 5 | const metadata = await getRemotePackageMetadata(packageName); 6 | return metadata?.dependencies || null; 7 | } -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/npm/getRemotePackageMetadata.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | import { PackageMetadata } from "./PackageMetadata"; 3 | 4 | const cache = new Map(); 5 | 6 | export async function getRemotePackageMetadata(packageName: string): Promise { 7 | if (cache.has(packageName)) { 8 | return cache.get(packageName)!; 9 | } 10 | 11 | return new Promise((resolve) => { 12 | exec(`npm view ${packageName} --json`, (err, stdout) => { 13 | if (err) { 14 | resolve(null); 15 | } 16 | try { 17 | const metadata = JSON.parse(stdout) 18 | cache.set(packageName, metadata); 19 | resolve(metadata as PackageMetadata); 20 | } catch (e) { 21 | resolve(null); 22 | } 23 | }); 24 | }); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/npm/getRemotePackageVersion.ts: -------------------------------------------------------------------------------- 1 | import { getRemotePackageMetadata } from "./getRemotePackageMetadata"; 2 | 3 | 4 | export async function getRemotePackageVersion(packageName: string) { 5 | const metadata = await getRemotePackageMetadata(packageName); 6 | return metadata?.version || null; 7 | } -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/npm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './PackageMetadata'; 2 | export * from './getRemotePackageDependencies'; 3 | export * from './getRemotePackageMetadata'; 4 | export * from './getRemotePackageVersion'; 5 | -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/reflect/allApps.ts: -------------------------------------------------------------------------------- 1 | import { allProjects } from "./allProjects"; 2 | 3 | export function allApps() { 4 | const projects = allProjects() 5 | return projects.filter(p => p.projectRelativeFolder.startsWith("apps")); 6 | } -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/reflect/allLibraries.ts: -------------------------------------------------------------------------------- 1 | import { allProjects } from "./allProjects"; 2 | 3 | export function allLibraries() { 4 | const projects = allProjects() 5 | return projects.filter(p => p.projectRelativeFolder.startsWith("libs")); 6 | } -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/reflect/allProjects.ts: -------------------------------------------------------------------------------- 1 | import { RushConfiguration } from "@microsoft/rush-lib"; 2 | 3 | export function allProjects() { 4 | const conf = RushConfiguration.loadFromDefaultLocation({ 5 | startingFolder: process.cwd() 6 | }); 7 | return conf.projects 8 | } -------------------------------------------------------------------------------- /rigs/repo-scripts/src/utils/reflect/index.ts: -------------------------------------------------------------------------------- 1 | export * from './allLibraries'; 2 | export * from './allProjects'; 3 | export * from './allApps'; 4 | -------------------------------------------------------------------------------- /rigs/repo-scripts/src/versions.ts: -------------------------------------------------------------------------------- 1 | import { RushConfiguration, RushConfigurationProject as RCP } from "@microsoft/rush-lib" 2 | import { green, yellow } from "colors"; 3 | import { table } from "table"; 4 | import { allLibraries, getRemotePackageVersion } from "./utils"; 5 | 6 | type PackageInfo = { 7 | name: string; 8 | localVersion: string; 9 | publishedVersion: string; 10 | } 11 | 12 | async function getPackageVersions(project: RCP): Promise { 13 | const localVersion = project.packageJson.version; 14 | const remoteVersion = await getRemotePackageVersion(project.packageName); 15 | const publishedVersion = remoteVersion || "Not published"; 16 | return { 17 | name: project.packageName, 18 | localVersion, 19 | publishedVersion 20 | }; 21 | } 22 | 23 | function toArray(info: PackageInfo) { 24 | const { name, localVersion, publishedVersion } = info; 25 | const title = localVersion === publishedVersion ? green(name) : yellow(name); 26 | return [title, localVersion, publishedVersion]; 27 | } 28 | 29 | function toTable(packages: PackageInfo[]) { 30 | const rows = packages.map(toArray); 31 | return table([ 32 | ["Package", "Local", "Published"], 33 | ...rows 34 | ]) 35 | } 36 | 37 | async function main() { 38 | const libs = allLibraries(); 39 | const packages = await Promise.all(libs.map(getPackageVersions)); 40 | console.log(toTable(packages)); 41 | } 42 | 43 | main(); -------------------------------------------------------------------------------- /rigs/repo-scripts/templates/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "sourceMap": true, 8 | "removeComments": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "pretty": true, 15 | "stripInternal": true, 16 | "preserveSymlinks": true, 17 | "outDir": "./dist", 18 | "incremental": true, 19 | "resolveJsonModule": true, 20 | "types": [ 21 | "node", 22 | "jest", 23 | "reflect-metadata" 24 | ], 25 | "experimentalDecorators": true, 26 | "emitDecoratorMetadata": true, 27 | "skipLibCheck": true, 28 | "lib": ["ES2021.WeakRef"] 29 | }, 30 | "include": [ 31 | "./src", 32 | ], 33 | "exclude": [ 34 | "./node_modules", 35 | "src/**/*.test.ts" 36 | ] 37 | } -------------------------------------------------------------------------------- /rigs/repo-scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/node-rig/profiles/default/tsconfig-base.json", 3 | "exclude": [ 4 | "node_modules" 5 | ] 6 | } --------------------------------------------------------------------------------