├── .github └── workflows │ └── basic-ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build ├── index.js ├── mother-agent.js ├── scenario-agent.js └── util │ ├── agent-utils.js │ ├── branch-manager.js │ ├── logger.js │ ├── mcp.js │ ├── membank.js │ ├── observations.js │ ├── reports.js │ └── sanitize.js ├── ci ├── get-project-id.js └── mcp-client │ ├── build │ └── index.js │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── config └── tools.json ├── core-files.txt ├── deebo_logo.jpeg ├── gen.sh ├── memory-bank ├── 7d4cacd8ed6f │ ├── activeContext.md │ └── progress.md └── af72caf9ed17 │ ├── activeContext.md │ └── progress.md ├── package-lock.json ├── package.json ├── packages ├── deebo-doctor │ ├── build │ │ ├── checks.js │ │ ├── index.js │ │ └── types.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── checks.ts │ │ ├── index.ts │ │ └── types.ts │ └── tsconfig.json └── deebo-setup │ ├── build │ ├── deebo_guide.md │ ├── guide-server.js │ ├── guide-server │ │ └── index.js │ ├── guide-setup.js │ ├── index.js │ ├── types.js │ └── utils.js │ ├── package-lock.json │ ├── package.json │ ├── src │ ├── deebo_guide.md │ ├── guide-server.ts │ ├── guide-setup.ts │ ├── index.ts │ ├── types.ts │ └── utils.ts │ └── tsconfig.json ├── src ├── index.ts ├── mother-agent.ts ├── scenario-agent.ts └── util │ ├── agent-utils.ts │ ├── branch-manager.ts │ ├── logger.ts │ ├── mcp.ts │ ├── membank.ts │ ├── observations.ts │ ├── reports.ts │ └── sanitize.ts └── tsconfig.json /.github/workflows/basic-ci.yml: -------------------------------------------------------------------------------- 1 | name: Deebo CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - fix-ci-env-vars 8 | pull_request: 9 | branches: 10 | - master 11 | - fix-ci-env-vars 12 | 13 | jobs: 14 | basic-test: 15 | runs-on: macos-latest 16 | timeout-minutes: 25 17 | env: 18 | NODE_ENV: development 19 | USE_MEMORY_BANK: "true" 20 | MOTHER_HOST: openrouter 21 | MOTHER_MODEL: anthropic/claude-3.5-sonnet 22 | SCENARIO_HOST: openrouter 23 | SCENARIO_MODEL: deepseek/deepseek-chat 24 | OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} 25 | 26 | steps: 27 | - name: Checkout Deebo code 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 1 31 | 32 | - name: Set up Node.js 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: '18' 36 | cache: 'npm' 37 | 38 | - name: Install ripgrep (macOS) 39 | run: | 40 | brew install ripgrep 41 | echo "RIPGREP_PATH=$(which rg)" >> $GITHUB_ENV 42 | 43 | - name: Install uv (macOS) 44 | run: | 45 | curl -LsSf https://astral.sh/uv/install.sh | sh 46 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 47 | 48 | - name: Determine tool paths 49 | id: tool_paths 50 | shell: bash 51 | run: | 52 | find_command() { 53 | local cmd=$1 54 | which "$cmd" 55 | } 56 | NPX_PATH=$(find_command npx) 57 | UVX_PATH=$(find_command uvx) 58 | NPM_BIN=$(dirname "$NPX_PATH") 59 | echo "DEEBO_NPX_PATH=${NPX_PATH}" >> $GITHUB_ENV 60 | echo "DEEBO_UVX_PATH=${UVX_PATH}" >> $GITHUB_ENV 61 | echo "DEEBO_NPM_BIN=${NPM_BIN}" >> $GITHUB_ENV 62 | 63 | - name: Install Deebo dependencies 64 | env: 65 | RIPGREP_PATH: ${{ env.RIPGREP_PATH }} 66 | NPM_CONFIG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | run: | 69 | echo "//api.github.com/:_authToken=${GITHUB_TOKEN}" >> ~/.npmrc 70 | npm install --loglevel error --no-optional 71 | 72 | - name: Build Deebo 73 | run: npm run build 74 | 75 | - name: Build minimal MCP client 76 | working-directory: ci/mcp-client 77 | env: 78 | RIPGREP_PATH: ${{ env.RIPGREP_PATH }} 79 | NPM_CONFIG_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | run: | 82 | echo "//api.github.com/:_authToken=${GITHUB_TOKEN}" >> ~/.npmrc 83 | npm install --loglevel error --no-optional 84 | npm run build 85 | 86 | - name: Clone task manager fixture repo 87 | run: | 88 | rm -rf task-manager-fixture 89 | git clone https://github.com/snagasuri/task-manager.git task-manager-fixture 90 | 91 | - name: Check OpenRouter API key status 92 | env: 93 | OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} 94 | run: | 95 | curl -fsS -G "https://openrouter.ai/api/v1/auth/key" \ 96 | -H "Authorization: Bearer $OPENROUTER_API_KEY" \ 97 | -o key_status.json 98 | jq -e '.data.label' key_status.json 99 | 100 | - name: Run Deebo server and test client (direct tool calls) 101 | id: deebo_run 102 | shell: bash 103 | run: | 104 | set -e 105 | DEEBO_BUILD=$(pwd)/build/index.js 106 | CLIENT_BUILD=$(pwd)/ci/mcp-client/build/index.js 107 | FIXTURE=$(pwd)/task-manager-fixture 108 | rm -rf memory-bank 109 | 110 | echo "Launching Deebo server..." 111 | node "$DEEBO_BUILD" --once & 112 | SERVER_PID=$! 113 | echo "Deebo server PID: $SERVER_PID" 114 | sleep 2 115 | 116 | echo "Running client against server..." 117 | node "$CLIENT_BUILD" "$DEEBO_BUILD" "$FIXTURE" | tee client_output.log 118 | CLIENT_EXIT=${PIPESTATUS[0]} 119 | if [[ $CLIENT_EXIT -ne 0 ]]; then 120 | echo "Client failed with code $CLIENT_EXIT" 121 | kill $SERVER_PID || true 122 | exit $CLIENT_EXIT 123 | fi 124 | 125 | echo "Stopping Deebo server..." 126 | kill $SERVER_PID || true 127 | wait $SERVER_PID 2>/dev/null || true 128 | 129 | SESSION_ID=$(grep 'FINAL_SESSION_ID_MARKER:' client_output.log | cut -d':' -f2) 130 | SESSION_ID=${SESSION_ID:-$(grep '✅ Started session:' client_output.log | sed 's/✅ Started session: //')} 131 | PROJECT_ID=$(node --experimental-specifier-resolution=node ci/get-project-id.js "$FIXTURE") 132 | 133 | echo "PROJECT_ID=$PROJECT_ID" >> $GITHUB_ENV 134 | echo "SESSION_ID=$SESSION_ID" >> $GITHUB_ENV 135 | 136 | - name: Debug logs folder 137 | run: | 138 | echo "PROJECT_ID=$PROJECT_ID SESSION_ID=$SESSION_ID" 139 | ls -R memory-bank/${PROJECT_ID}/sessions/${SESSION_ID} 140 | 141 | - name: Upload session logs 142 | uses: actions/upload-artifact@v4 143 | with: 144 | name: session-logs 145 | path: memory-bank/${{ env.PROJECT_ID }}/sessions/${{ env.SESSION_ID }}/logs/ 146 | if-no-files-found: error 147 | retention-days: 7 148 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules/ 3 | 4 | # Local env files 5 | .env 6 | .env.local 7 | .env.*.local 8 | 9 | # Logs 10 | logs/ 11 | *.log 12 | 13 | # Build outputs (for root only) 14 | dist/ 15 | 16 | # Editor and OS files 17 | .idea/ 18 | .vscode/ 19 | *.swp 20 | *.swo 21 | .DS_Store 22 | 23 | # Test coverage 24 | coverage/ 25 | 26 | # npm cache 27 | .npm/ 28 | 29 | # eslint cache 30 | .eslintcache 31 | 32 | # Python virtual environments 33 | venv/ 34 | 35 | # Project-specific folders 36 | workers/ 37 | notes/ 38 | TODO.md 39 | 40 | # Deebo runtime output 41 | memory-bank/ 42 | reports/ 43 | cicd-files.txt 44 | gen-cicd.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2025 Sriram Nagasuri 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deebo: Your AI Agent's Debugging Copilot 2 | [![CI Status](https://github.com/snagasuri/deebo-prototype/actions/workflows/basic-ci.yml/badge.svg)](https://github.com/snagasuri/deebo-prototype/actions/workflows/basic-ci.yml) 3 | [![npm version](https://img.shields.io/npm/v/deebo-setup.svg)](https://www.npmjs.com/package/deebo-setup) 4 | [![GitHub stars](https://img.shields.io/github/stars/snagasuri/deebo-prototype?style=social)](https://github.com/snagasuri/deebo-prototype) 5 | 6 | Deebo is an agentic debugging copilot for your AI coding agent that speeds up time-to-resolution by 10x. If your main coding agent is like a single-threaded process, Deebo introduces multi-threadedness to your development workflow. 7 | 8 | **feedback, questions/support? check out Deebo Guide below, or dm me on x @sriramenn** 9 | 10 | **If you think your team can benefit from Deebo, we’d love to hear from you.** We’re partnering with teams who use AI agents to write production code and want to maximize their productivity. Reach out for a live walkthrough, custom setup support, or to explore early access to enterprise features. 11 | 12 | ## Quick Install 13 | 14 | ```bash 15 | npx deebo-setup@latest 16 | ``` 17 | 18 |
19 | Manual Configuration 20 | 21 | After installing with deebo-setup, create a configuration file at your coding agent's specified location with the following content. First, add the guide server (which provides help documentation even if the main installation fails): 22 | 23 | ```json 24 | { 25 | "servers": { 26 | "deebo-guide": { 27 | "command": "node", 28 | "args": [ 29 | "--experimental-specifier-resolution=node", 30 | "--experimental-modules", 31 | "/Users/[your-name]/.deebo/guide-server.js" 32 | ], 33 | "env": {}, 34 | "transportType": "stdio" 35 | }, 36 | "deebo": { 37 | "command": "node", 38 | "args": [ 39 | "--experimental-specifier-resolution=node", 40 | "--experimental-modules", 41 | "--max-old-space-size=4096", 42 | "/Users/[your-name]/.deebo/build/index.js" 43 | ], 44 | "env": { 45 | "NODE_ENV": "development", 46 | "USE_MEMORY_BANK": "true", 47 | "MOTHER_HOST": "openrouter", 48 | "MOTHER_MODEL": "anthropic/claude-3.5-sonnet", 49 | "SCENARIO_HOST": "openrouter", 50 | "SCENARIO_MODEL": "deepseek/deepseek-chat", 51 | "OPENROUTER_API_KEY": "your-openrouter-api-key" 52 | } 53 | } 54 | } 55 | } 56 | ``` 57 | Deebo works with any OpenAI-compatible SDK, Anthropic, Gemini, and OpenRouter. 58 |
59 | 60 |
61 | 62 | Deebo Guide 63 | 64 | Deebo helps your AI agent debug real software errors by launching automated investigations. Here's how to use it effectively. 65 | 66 | --- 67 | 68 | ### 1. Start a Debugging Session 69 | 70 | When you hit a tough bug, ask your agent to delegate the task to Deebo. 71 | 72 | **What to include in your request:** 73 | - 🔧 The **error** (message, stack trace, or behavior) 74 | - 📁 The **absolute path** to your Git repository 75 | - 💡 Any helpful **context**, such as: 76 | - What you’ve already tried 77 | - Relevant files or code snippets 78 | - How to reproduce the issue 79 | - The language or environment 80 | 81 | **Example instruction to your agent:** 82 | 83 | > “This error is happening in `/path/to/repo`, possibly related to auth logic. I already checked the session token parser. Can you delegate this to Deebo?” 84 | 85 | Your agent will start a Deebo session and give you a **session ID** (e.g. `session-1745...`). Save it. 86 | 87 | --- 88 | 89 | ### 2. Check Investigation Progress 90 | 91 | After ~30 seconds, ask your agent to check the status of the Deebo session using that session ID. 92 | 93 | You’ll get a **session pulse**, which shows: 94 | - Whether the investigation is in progress or completed 95 | - What the system is currently exploring 96 | - Summaries of findings, if any 97 | 98 | --- 99 | 100 | ### 3. Add Observations (Optional) 101 | 102 | If you notice something important — or think Deebo is heading the wrong way — you can guide the investigation. 103 | 104 | Ask your agent to pass a short observation to Deebo. 105 | 106 | **Example:** 107 | 108 | > “Let Deebo know that the file size warnings might be a red herring — the issue is probably with the CI env vars.” 109 | 110 | This may shift the direction of the investigation. 111 | 112 | ### 4. Cancel a Session (Optional) 113 | 114 | If you fixed the issue or no longer need the investigation, tell your agent to cancel the Deebo session. 115 | 116 | ### For AI Agents: Memory Bank Access 117 | 118 | When asked to check debug session progress, look in: 119 | `~/.deebo/memory-bank/[project-hash]/sessions/[session-id]/logs/` 120 | 121 | The project hash is a unique identifier for each repository, and session IDs are provided when starting a debug session. 122 | 123 | ### Want More? 124 | 125 | We're piloting enterprise features that unlock unprecedented productivity gains for your team. Reach out if interested! 126 |
127 | 128 | --- 129 | [Watch the full work session with Cline + Deebo here (3 mins, sped up)](https://drive.google.com/file/d/141VdQ9DNOfnOpP_mmB0UPMr8cwAGrxKC/view) 130 | 131 | 132 | 133 | ## License 134 | 135 | Apache License 2.0 — see [LICENSE](LICENSE) for details. 136 | -------------------------------------------------------------------------------- /build/scenario-agent.js: -------------------------------------------------------------------------------- 1 | // src/scenario-agent.ts 2 | import { log } from './util/logger.js'; 3 | import { connectRequiredTools } from './util/mcp.js'; 4 | import { writeReport } from './util/reports.js'; 5 | import { getAgentObservations } from './util/observations.js'; 6 | import { callLlm, getScenarioAgentPrompt } from './util/agent-utils.js'; 7 | const MAX_RUNTIME = 15 * 60 * 1000; // 15 minutes 8 | function parseArgs(args) { 9 | const result = {}; 10 | for (let i = 0; i < args.length; i++) { 11 | if (args[i].startsWith('--')) { 12 | const key = args[i].slice(2); 13 | const value = args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : ''; 14 | result[key] = value; 15 | if (value) 16 | i++; 17 | } 18 | } 19 | const repoPath = result.repo; 20 | if (!repoPath) { 21 | throw new Error('Required argument missing: --repo'); 22 | } 23 | return { 24 | id: result.id || '', 25 | session: result.session || '', 26 | error: result.error || '', 27 | context: result.context || '', 28 | hypothesis: result.hypothesis || '', 29 | language: result.language || 'typescript', 30 | repoPath, 31 | filePath: result.file || undefined, 32 | branch: result.branch || '' 33 | }; 34 | } 35 | export async function runScenarioAgent(args) { 36 | await log(args.session, `scenario-${args.id}`, 'info', 'Scenario agent started', { repoPath: args.repoPath, hypothesis: args.hypothesis }); 37 | await log(args.session, `scenario-${args.id}`, 'debug', `CWD: ${process.cwd()}, DEEBO_NPX_PATH=${process.env.DEEBO_NPX_PATH}, DEEBO_UVX_PATH=${process.env.DEEBO_UVX_PATH}`, { repoPath: args.repoPath }); 38 | try { 39 | // Set up tools 40 | await log(args.session, `scenario-${args.id}`, 'info', 'Connecting to tools...', { repoPath: args.repoPath }); 41 | const { gitClient, filesystemClient } = await connectRequiredTools(`scenario-${args.id}`, args.session, args.repoPath); 42 | await log(args.session, `scenario-${args.id}`, 'info', 'Connected to tools successfully', { repoPath: args.repoPath }); 43 | // Branch creation is handled by system infrastructure before this agent is spawned. 44 | // Start LLM conversation with initial context 45 | const startTime = Date.now(); 46 | // Initial conversation context 47 | const messages = [{ 48 | role: 'assistant', 49 | content: getScenarioAgentPrompt({ 50 | branch: args.branch, 51 | hypothesis: args.hypothesis, 52 | context: args.context, 53 | repoPath: args.repoPath 54 | }) 55 | }, { 56 | role: 'user', 57 | content: `Error: ${args.error} 58 | Context: ${args.context} 59 | Language: ${args.language} 60 | File: ${args.filePath} 61 | Repo: ${args.repoPath} 62 | Hypothesis: ${args.hypothesis}` 63 | }]; 64 | // Check for observations (initial load) 65 | let observations = await getAgentObservations(args.repoPath, args.session, `scenario-${args.id}`); 66 | if (observations.length > 0) { 67 | messages.push(...observations.map((obs) => ({ 68 | role: 'user', 69 | content: `Scientific observation: ${obs}` 70 | }))); 71 | } 72 | // Read LLM configuration from environment variables 73 | const scenarioProvider = process.env.SCENARIO_HOST; // Read provider name from SCENARIO_HOST 74 | const scenarioModel = process.env.SCENARIO_MODEL; 75 | const openrouterApiKey = process.env.OPENROUTER_API_KEY; // Still needed if provider is 'openrouter' 76 | const openaiApiKey = process.env.OPENAI_API_KEY; 77 | const openaiBaseUrl = process.env.OPENAI_BASE_URL; 78 | const geminiApiKey = process.env.GEMINI_API_KEY; 79 | const anthropicApiKey = process.env.ANTHROPIC_API_KEY; 80 | // Create the config object to pass to callLlm 81 | const llmConfig = { 82 | provider: scenarioProvider, // Use the provider name from SCENARIO_HOST 83 | model: scenarioModel, 84 | apiKey: openrouterApiKey, 85 | openrouterApiKey: openrouterApiKey, // For OpenRouter 86 | openaiApiKey: openaiApiKey, // For OpenAI and compatible providers 87 | baseURL: openaiBaseUrl, // For OpenAI-compatible APIs 88 | geminiApiKey: geminiApiKey, 89 | anthropicApiKey: anthropicApiKey 90 | }; 91 | await log(args.session, `scenario-${args.id}`, 'debug', 'Sending to LLM', { model: llmConfig.model, provider: llmConfig.provider, messages, repoPath: args.repoPath }); 92 | // Add retry logic with exponential backoff for initial call 93 | let consecutiveFailures = 0; 94 | const MAX_RETRIES = 3; 95 | let replyText; 96 | while (consecutiveFailures < MAX_RETRIES) { 97 | replyText = await callLlm(messages, llmConfig); 98 | if (!replyText) { 99 | // Log the failure and increment counter 100 | consecutiveFailures++; 101 | await log(args.session, `scenario-${args.id}`, 'warn', `Received empty/malformed response from LLM on initial call (Failure ${consecutiveFailures}/${MAX_RETRIES})`, { provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 102 | // Push a message indicating the failure to help LLM recover 103 | messages.push({ 104 | role: 'user', 105 | content: `INTERNAL_NOTE: Initial LLM call failed to return valid content (Attempt ${consecutiveFailures}/${MAX_RETRIES}). Please try again.` 106 | }); 107 | // Add exponential backoff delay 108 | const delay = 2000 * Math.pow(2, consecutiveFailures - 1); 109 | await new Promise(resolve => setTimeout(resolve, delay)); 110 | // Try again if we haven't hit max retries 111 | if (consecutiveFailures < MAX_RETRIES) { 112 | continue; 113 | } 114 | // Max retries hit - write report and exit 115 | const errorMsg = `Initial LLM call failed to return valid response after ${MAX_RETRIES} attempts`; 116 | await log(args.session, `scenario-${args.id}`, 'error', errorMsg, { provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 117 | await writeReport(args.repoPath, args.session, args.id, errorMsg); 118 | console.log(errorMsg); 119 | process.exit(1); 120 | } 121 | // Valid response received 122 | messages.push({ role: 'assistant', content: replyText }); 123 | await log(args.session, `scenario-${args.id}`, 'debug', 'Received response from LLM', { response: { content: replyText }, repoPath: args.repoPath }); 124 | break; // Exit retry loop on success 125 | } 126 | // --- Main Investigation Loop --- 127 | while (true) { 128 | if (Date.now() - startTime > MAX_RUNTIME) { 129 | const timeoutMsg = 'Investigation exceeded maximum runtime'; 130 | await log(args.session, `scenario-${args.id}`, 'warn', timeoutMsg, { repoPath: args.repoPath }); 131 | await writeReport(args.repoPath, args.session, args.id, timeoutMsg); 132 | console.log(timeoutMsg); 133 | process.exit(1); 134 | } 135 | // Get the latest assistant response 136 | if (!replyText) { 137 | const errorMsg = 'Unexpected undefined response in main loop'; 138 | await log(args.session, `scenario-${args.id}`, 'error', errorMsg, { repoPath: args.repoPath }); 139 | await writeReport(args.repoPath, args.session, args.id, errorMsg); 140 | console.log(errorMsg); 141 | process.exit(1); 142 | } 143 | // --- Check for Report and Tool Calls --- 144 | const toolCalls = replyText.match(/[\s\S]*?<\/use_mcp_tool>/g) || []; 145 | const reportMatch = replyText.match(/\s*([\s\S]*?)<\/report>/i); 146 | let executeToolsThisTurn = false; 147 | let exitThisTurn = false; 148 | if (reportMatch && toolCalls.length > 0) { 149 | // LLM included both - prioritize executing tools, ignore report this turn 150 | messages.push({ 151 | role: 'user', 152 | content: `Instructions conflict: You provided tool calls and a report in the same message. I will execute the tool calls now. Provide the report ONLY after analyzing the tool results in the next turn.` 153 | }); 154 | executeToolsThisTurn = true; // Signal to execute tools below 155 | await log(args.session, `scenario-${args.id}`, 'warn', 'LLM provided tools and report simultaneously. Executing tools, ignoring report.', { repoPath: args.repoPath }); 156 | } 157 | else if (reportMatch) { 158 | // Only report found - process it and exit 159 | const reportText = reportMatch[1].trim(); 160 | await log(args.session, `scenario-${args.id}`, 'info', 'Report found. Writing report and exiting.', { repoPath: args.repoPath }); 161 | await writeReport(args.repoPath, args.session, args.id, reportText); 162 | console.log(reportText); // Print report to stdout for mother agent 163 | exitThisTurn = true; // Signal to exit loop cleanly 164 | } 165 | else if (toolCalls.length > 0) { 166 | // Only tool calls found - execute them 167 | executeToolsThisTurn = true; // Signal to execute tools below 168 | await log(args.session, `scenario-${args.id}`, 'debug', `Found ${toolCalls.length} tool calls to execute.`, { repoPath: args.repoPath }); 169 | } 170 | // If neither tools nor report found, the loop continues to the next LLM call 171 | // Exit now if a report-only response was processed 172 | if (exitThisTurn) { 173 | process.exit(0); 174 | } 175 | // --- Execute Tools if Flagged --- 176 | if (executeToolsThisTurn) { 177 | const parsedCalls = toolCalls.map((tc) => { 178 | try { 179 | const serverNameMatch = tc.match(/(.*?)<\/server_name>/); 180 | if (!serverNameMatch || !serverNameMatch[1]) 181 | throw new Error('Missing server_name'); 182 | const serverName = serverNameMatch[1]; 183 | const server = serverName === 'git-mcp' ? gitClient : filesystemClient; // Select client based on name 184 | if (!server) 185 | throw new Error(`Invalid server_name: ${serverName}`); 186 | const toolMatch = tc.match(/(.*?)<\/tool_name>/); 187 | if (!toolMatch || !toolMatch[1]) 188 | throw new Error('Missing tool_name'); 189 | const tool = toolMatch[1]; 190 | const argsMatch = tc.match(/(.*?)<\/arguments>/s); 191 | if (!argsMatch || !argsMatch[1]) 192 | throw new Error('Missing arguments'); 193 | const args = JSON.parse(argsMatch[1]); 194 | return { server, tool, args }; 195 | } 196 | catch (err) { 197 | const errorMsg = err instanceof Error ? err.message : String(err); 198 | log(args.session, `scenario-${args.id}`, 'error', `Failed to parse tool call: ${errorMsg}`, { toolCall: tc, repoPath: args.repoPath }); 199 | return { error: errorMsg }; // Return error object for specific call 200 | } 201 | }); 202 | // Process each parsed call - add results or errors back to messages 203 | let toolCallFailed = false; 204 | for (const parsed of parsedCalls) { 205 | if ('error' in parsed) { 206 | messages.push({ 207 | role: 'user', 208 | content: `Tool call parsing failed: ${parsed.error}` 209 | }); 210 | toolCallFailed = true; // Mark failure, but continue processing other calls if needed, or let LLM handle it next turn 211 | continue; // Skip execution for this malformed call 212 | } 213 | // Prevent disallowed tools 214 | if (parsed.tool === 'git_create_branch') { 215 | messages.push({ 216 | role: 'user', 217 | content: 'Error: Tool call `git_create_branch` is not allowed. The branch was already created by the mother agent.' 218 | }); 219 | await log(args.session, `scenario-${args.id}`, 'warn', `Attempted disallowed tool call: ${parsed.tool}`, { repoPath: args.repoPath }); 220 | continue; // Skip this specific call 221 | } 222 | try { 223 | await log(args.session, `scenario-${args.id}`, 'debug', `Executing tool: ${parsed.tool}`, { args: parsed.args, repoPath: args.repoPath }); 224 | const result = await parsed.server.callTool({ name: parsed.tool, arguments: parsed.args }); 225 | messages.push({ 226 | role: 'user', 227 | content: JSON.stringify(result) // Tool results are added as user messages 228 | }); 229 | await log(args.session, `scenario-${args.id}`, 'debug', `Tool result for ${parsed.tool}`, { result: result, repoPath: args.repoPath }); 230 | } 231 | catch (toolErr) { 232 | const errorMsg = toolErr instanceof Error ? toolErr.message : String(toolErr); 233 | messages.push({ 234 | role: 'user', 235 | content: `Tool call failed for '${parsed.tool}': ${errorMsg}` 236 | }); 237 | await log(args.session, `scenario-${args.id}`, 'error', `Tool call execution failed: ${parsed.tool}`, { error: errorMsg, repoPath: args.repoPath }); 238 | toolCallFailed = true; // Mark failure 239 | } 240 | } 241 | // Decide if we should immediately ask LLM again after tool failure, or let the loop naturally continue. 242 | // Current logic lets loop continue, LLM will see the error messages. 243 | } 244 | // --- Check for New Observations --- 245 | const newObservations = await getAgentObservations(args.repoPath, args.session, `scenario-${args.id}`); 246 | if (newObservations.length > observations.length) { 247 | const latestObservations = newObservations.slice(observations.length); 248 | messages.push(...latestObservations.map((obs) => ({ 249 | role: 'user', 250 | content: `Scientific observation: ${obs}` 251 | }))); 252 | observations = newObservations; // Update the baseline observation list 253 | await log(args.session, `scenario-${args.id}`, 'debug', `Added ${latestObservations.length} new observations to context.`, { repoPath: args.repoPath }); 254 | } 255 | // --- Make Next LLM Call --- 256 | await log(args.session, `scenario-${args.id}`, 'debug', `Sending message history (${messages.length} items) to LLM`, { model: llmConfig.model, provider: llmConfig.provider, repoPath: args.repoPath }); 257 | // Add retry logic with exponential backoff 258 | let consecutiveFailures = 0; 259 | const MAX_RETRIES = 3; 260 | while (consecutiveFailures < MAX_RETRIES) { 261 | replyText = await callLlm(messages, llmConfig); 262 | if (!replyText) { 263 | // Log the failure and increment counter 264 | consecutiveFailures++; 265 | await log(args.session, `scenario-${args.id}`, 'warn', `Received empty/malformed response from LLM (Failure ${consecutiveFailures}/${MAX_RETRIES})`, { provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 266 | // Push a message indicating the failure to help LLM recover 267 | messages.push({ 268 | role: 'user', 269 | content: `INTERNAL_NOTE: Previous LLM call failed to return valid content (Attempt ${consecutiveFailures}/${MAX_RETRIES}). Please try again.` 270 | }); 271 | // Add exponential backoff delay 272 | const delay = 2000 * Math.pow(2, consecutiveFailures - 1); 273 | await new Promise(resolve => setTimeout(resolve, delay)); 274 | // Try again if we haven't hit max retries 275 | if (consecutiveFailures < MAX_RETRIES) { 276 | continue; 277 | } 278 | // Max retries hit - write report and exit 279 | const errorMsg = `LLM failed to return valid response after ${MAX_RETRIES} attempts`; 280 | await log(args.session, `scenario-${args.id}`, 'error', errorMsg, { provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 281 | await writeReport(args.repoPath, args.session, args.id, errorMsg); 282 | console.log(errorMsg); 283 | process.exit(1); 284 | } 285 | // Valid response received 286 | messages.push({ role: 'assistant', content: replyText }); 287 | await log(args.session, `scenario-${args.id}`, 'debug', 'Received response from LLM', { responseLength: replyText.length, provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 288 | break; // Exit retry loop on success 289 | } 290 | // Small delay before next iteration (optional) 291 | await new Promise(resolve => setTimeout(resolve, 1000)); 292 | } 293 | } 294 | catch (error) { 295 | // Catch unexpected errors during setup or within the loop if not handled 296 | const errorText = error instanceof Error ? `${error.message}${error.stack ? `\nStack: ${error.stack}` : ''}` : String(error); 297 | await log(args.session, `scenario-${args.id}`, 'error', `Unhandled scenario error: ${errorText}`, { repoPath: args.repoPath }); 298 | await writeReport(args.repoPath, args.session, args.id, `SCENARIO FAILED UNEXPECTEDLY: ${errorText}`); 299 | console.error(`SCENARIO FAILED UNEXPECTEDLY: ${errorText}`); // Log error to stderr as well 300 | process.exit(1); 301 | } 302 | } 303 | // --- Script Entry Point --- 304 | try { 305 | const args = parseArgs(process.argv.slice(2)); // Pass relevant args, skipping node path and script path 306 | runScenarioAgent(args); // No await here, let the async function run 307 | } 308 | catch (err) { 309 | // Handle argument parsing errors 310 | const errorText = err instanceof Error ? err.message : String(err); 311 | console.error(`Scenario agent failed to start due to arg parsing error: ${errorText}`); 312 | // Attempt to log if possible, though session info might be missing 313 | // log(args.session || 'unknown', `scenario-${args.id || 'unknown'}`, 'error', `Arg parsing failed: ${errorText}`, {}).catch(); 314 | process.exit(1); 315 | } 316 | // Optional: Add unhandled rejection/exception handlers for more robustness 317 | process.on('unhandledRejection', (reason, promise) => { 318 | console.error('Unhandled Rejection at:', promise, 'reason:', reason); 319 | // Log this? Might be hard without session context. 320 | process.exit(1); // Exit on unhandled promise rejection 321 | }); 322 | process.on('uncaughtException', (error) => { 323 | console.error('Uncaught Exception:', error); 324 | // Log this? 325 | process.exit(1); // Exit on uncaught exception 326 | }); 327 | -------------------------------------------------------------------------------- /build/util/branch-manager.js: -------------------------------------------------------------------------------- 1 | // src/util/branch-manager.ts 2 | import { simpleGit } from 'simple-git'; 3 | // note: second parameter is `scenarioId` 4 | export async function createScenarioBranch(repoPath, scenarioId) { 5 | const git = simpleGit(repoPath); 6 | const branchName = `debug-${scenarioId}`; // e.g. debug-session-1745287764331-0 7 | await git.checkoutLocalBranch(branchName); 8 | return branchName; 9 | } 10 | -------------------------------------------------------------------------------- /build/util/logger.js: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs/promises'; 2 | import { join } from 'path'; 3 | import { DEEBO_ROOT } from '../index.js'; 4 | import { getProjectId } from './sanitize.js'; 5 | // Write logs to memory bank structure 6 | export async function log(sessionId, name, level, message, data) { 7 | const entry = JSON.stringify({ 8 | timestamp: new Date().toISOString(), 9 | agent: name, 10 | level, 11 | message, 12 | data 13 | }) + '\n'; 14 | // Data will be written to memory-bank/projectId/sessions/sessionId/logs/agentName.log 15 | const projectId = getProjectId(data?.repoPath); 16 | if (projectId) { 17 | const logPath = join(DEEBO_ROOT, 'memory-bank', projectId, 'sessions', sessionId, 'logs', `${name}.log`); 18 | await writeFile(logPath, entry, { flag: 'a' }); 19 | } 20 | } 21 | // Simple console logging 22 | export function consoleLog(level, message, data) { 23 | console.log(`[${level}] ${message}`, data || ''); 24 | } 25 | -------------------------------------------------------------------------------- /build/util/mcp.js: -------------------------------------------------------------------------------- 1 | // src/util/mcp.ts 2 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 3 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 4 | import { readFile } from 'fs/promises'; 5 | import { join } from 'path'; 6 | import * as path from 'path'; 7 | import { DEEBO_ROOT } from '../index.js'; 8 | import { getProjectId } from './sanitize.js'; 9 | // Map to track active connections 10 | const activeConnections = new Map(); 11 | export async function connectMcpTool(name, toolName, sessionId, repoPath) { 12 | const rawConfig = JSON.parse(await readFile(join(DEEBO_ROOT, 'config', 'tools.json'), 'utf-8')); 13 | const def = rawConfig.tools[toolName]; 14 | const memoryPath = join(DEEBO_ROOT, 'memory-bank', getProjectId(repoPath)); 15 | const memoryRoot = join(DEEBO_ROOT, 'memory-bank'); 16 | /* --- WINDOWS-ONLY PATCH ----------------------------------------- */ 17 | if (process.platform === "win32" && toolName === "desktopCommander") { 18 | // Use the real *.cmd so the process owns stdin/stdout 19 | const cmdPath = path.join(process.env.DEEBO_NPM_BIN, "desktop-commander.cmd"); 20 | def.command = cmdPath; 21 | def.args = ["serve"]; // same behaviour as 'npx … serve' 22 | } 23 | /* ---------------------------------------------------------------- */ 24 | // Substitute npx/uvx paths directly in the command 25 | let command = def.command 26 | .replace(/{npxPath}/g, process.env.DEEBO_NPX_PATH) 27 | .replace(/{uvxPath}/g, process.env.DEEBO_UVX_PATH); 28 | // Replace placeholders in all args 29 | let args = def.args.map((arg) => arg 30 | .replace(/{repoPath}/g, repoPath) 31 | .replace(/{memoryPath}/g, memoryPath) 32 | .replace(/{memoryRoot}/g, memoryRoot)); 33 | // Handle environment variable substitutions 34 | if (def.env) { 35 | for (const [key, value] of Object.entries(def.env)) { 36 | if (typeof value === 'string') { 37 | def.env[key] = value 38 | .replace(/{ripgrepPath}/g, process.env.RIPGREP_PATH) 39 | .replace(/{repoPath}/g, repoPath) 40 | .replace(/{memoryPath}/g, memoryPath) 41 | .replace(/{memoryRoot}/g, memoryRoot); 42 | } 43 | } 44 | } 45 | // No shell: spawn the .cmd/binary directly on all platforms 46 | const options = {}; 47 | const transport = new StdioClientTransport({ 48 | command, 49 | args, 50 | ...options, 51 | env: { 52 | ...process.env, // Inherit all environment variables 53 | // Explicitly set critical variables 54 | NODE_ENV: process.env.NODE_ENV, 55 | USE_MEMORY_BANK: process.env.USE_MEMORY_BANK, 56 | MOTHER_HOST: process.env.MOTHER_HOST, 57 | MOTHER_MODEL: process.env.MOTHER_MODEL, 58 | SCENARIO_HOST: process.env.SCENARIO_HOST, 59 | SCENARIO_MODEL: process.env.SCENARIO_MODEL, 60 | OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY 61 | } 62 | }); 63 | const client = new Client({ name, version: '1.0.0' }, { capabilities: { tools: true } }); 64 | await client.connect(transport); 65 | return client; 66 | } 67 | export async function connectRequiredTools(agentName, sessionId, repoPath) { 68 | const [gitClient, filesystemClient] = await Promise.all([ 69 | connectMcpTool(`${agentName}-git`, 'git-mcp', sessionId, repoPath), 70 | // Switch from "filesystem-mcp" to "desktop-commander" 71 | connectMcpTool(`${agentName}-desktop-commander`, 'desktopCommander', sessionId, repoPath) 72 | ]); 73 | return { gitClient, filesystemClient }; 74 | } 75 | -------------------------------------------------------------------------------- /build/util/membank.js: -------------------------------------------------------------------------------- 1 | // src/util/membank.js 2 | import { join } from 'path'; 3 | import { writeFile } from 'fs/promises'; 4 | import { DEEBO_ROOT } from '../index.js'; 5 | export async function updateMemoryBank(projectId, content, file) { 6 | const path = join(DEEBO_ROOT, 'memory-bank', projectId, `${file}.md`); 7 | await writeFile(path, '\n' + content, { flag: 'a' }); 8 | } 9 | -------------------------------------------------------------------------------- /build/util/observations.js: -------------------------------------------------------------------------------- 1 | import { writeFile, mkdir, readFile } from 'fs/promises'; 2 | import { join } from 'path'; 3 | import { DEEBO_ROOT } from '../index.js'; 4 | import { getProjectId } from './sanitize.js'; 5 | export async function getAgentObservations(repoPath, sessionId, agentId) { 6 | const projectId = getProjectId(repoPath); 7 | const obsPath = join(DEEBO_ROOT, 'memory-bank', projectId, 'sessions', sessionId, 'observations', `${agentId}.log`); 8 | try { 9 | const content = await readFile(obsPath, 'utf8'); 10 | return content 11 | .split('\n') 12 | .filter(Boolean) 13 | .map((line) => JSON.parse(line).observation); 14 | } 15 | catch { 16 | return []; // No observations yet 17 | } 18 | } 19 | export async function writeObservation(repoPath, sessionId, agentId, observation) { 20 | const projectId = getProjectId(repoPath); 21 | const obsDir = join(DEEBO_ROOT, 'memory-bank', projectId, 'sessions', sessionId, 'observations'); 22 | await mkdir(obsDir, { recursive: true }); 23 | const entry = JSON.stringify({ 24 | timestamp: new Date().toISOString(), 25 | observation 26 | }) + '\n'; 27 | await writeFile(join(obsDir, `${agentId}.log`), entry, { flag: 'a' }); 28 | } 29 | -------------------------------------------------------------------------------- /build/util/reports.js: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from "fs/promises"; 2 | import { join } from "path"; 3 | import { DEEBO_ROOT } from "../index.js"; 4 | import { getProjectId } from "./sanitize.js"; 5 | export async function writeReport(repoPath, sessionId, scenarioId, report) { 6 | const projectId = getProjectId(repoPath); 7 | const reportDir = join(DEEBO_ROOT, "memory-bank", projectId, "sessions", sessionId, "reports"); 8 | await mkdir(reportDir, { recursive: true }); 9 | // pretty-print with 2-space indent 10 | const reportPath = join(reportDir, `${scenarioId}.json`); 11 | await writeFile(reportPath, JSON.stringify(report, null, 2), "utf8"); 12 | } 13 | -------------------------------------------------------------------------------- /build/util/sanitize.js: -------------------------------------------------------------------------------- 1 | // src/util/sanitize.ts 2 | import { createHash } from 'crypto'; 3 | export function getProjectId(repoPath) { 4 | const hash = createHash('sha256').update(repoPath).digest('hex'); 5 | return hash.slice(0, 12); // use first 12 characters 6 | } 7 | -------------------------------------------------------------------------------- /ci/get-project-id.js: -------------------------------------------------------------------------------- 1 | import { getProjectId } from '../build/util/sanitize.js'; 2 | 3 | const fixture = process.argv[2]; 4 | if (!fixture) { 5 | console.error('Please provide the fixture path as an argument'); 6 | process.exit(1); 7 | } 8 | 9 | console.log(await getProjectId(fixture)); 10 | -------------------------------------------------------------------------------- /ci/mcp-client/build/index.js: -------------------------------------------------------------------------------- 1 | // ci/mcp-client/index.ts 2 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 3 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 4 | import OpenAI from "openai"; 5 | import dotenv from "dotenv"; 6 | import path from 'path'; 7 | dotenv.config(); 8 | const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; 9 | const CI_LLM_MODEL = process.env.CI_LLM_MODEL || "deepseek/deepseek-chat"; // Keep for optional analysis 10 | if (!OPENROUTER_API_KEY) { 11 | throw new Error("OPENROUTER_API_KEY environment variable is not set"); 12 | } 13 | const openrouterClient = new OpenAI({ 14 | apiKey: OPENROUTER_API_KEY, 15 | baseURL: "https://openrouter.ai/api/v1", 16 | }); 17 | class MinimalMCPClient { 18 | mcp; 19 | transport = null; 20 | connected = false; // Track connection status internally 21 | constructor() { 22 | this.mcp = new Client({ name: "deebo-ci-client", version: "1.0.0" }); 23 | } 24 | async connectToServer(deeboServerScriptPath) { 25 | if (this.connected) { 26 | console.log("CI Client already connected."); 27 | return; 28 | } 29 | if (!path.isAbsolute(deeboServerScriptPath)) { 30 | throw new Error(`Server script path must be absolute: ${deeboServerScriptPath}`); 31 | } 32 | try { 33 | // Ensure all required environment variables are present 34 | const requiredEnvVars = [ 35 | 'NODE_ENV', 36 | 'USE_MEMORY_BANK', 37 | 'MOTHER_HOST', 38 | 'MOTHER_MODEL', 39 | 'SCENARIO_HOST', 40 | 'SCENARIO_MODEL', 41 | 'OPENROUTER_API_KEY' 42 | ]; 43 | for (const envVar of requiredEnvVars) { 44 | if (!process.env[envVar]) { 45 | throw new Error(`Required environment variable ${envVar} is not set`); 46 | } 47 | } 48 | this.transport = new StdioClientTransport({ 49 | command: process.execPath, 50 | args: [ 51 | "--experimental-specifier-resolution=node", 52 | "--experimental-modules", 53 | "--max-old-space-size=4096", 54 | deeboServerScriptPath 55 | ], 56 | env: { 57 | ...process.env, // Inherit all environment variables from parent process 58 | // Explicitly set critical variables to ensure they're passed correctly 59 | NODE_ENV: process.env.NODE_ENV, 60 | USE_MEMORY_BANK: process.env.USE_MEMORY_BANK, 61 | MOTHER_HOST: process.env.MOTHER_HOST, 62 | MOTHER_MODEL: process.env.MOTHER_MODEL, 63 | SCENARIO_HOST: process.env.SCENARIO_HOST, 64 | SCENARIO_MODEL: process.env.SCENARIO_MODEL, 65 | OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY 66 | } 67 | }); 68 | const connectPromise = this.mcp.connect(this.transport); 69 | const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Connection timed out after 15 seconds")), 15000)); 70 | await Promise.race([connectPromise, timeoutPromise]); 71 | this.connected = true; // Set connected flag 72 | console.log("CI Client Connected to Deebo Server"); 73 | } 74 | catch (e) { 75 | this.connected = false; // Ensure flag is false on error 76 | console.error("CI Client Failed to connect to MCP server: ", e); 77 | throw e; 78 | } 79 | } 80 | ensureConnected() { 81 | if (!this.connected || !this.transport) { 82 | throw new Error("Client is not connected to the Deebo server."); 83 | } 84 | } 85 | async forceStartSession(args) { 86 | this.ensureConnected(); // Check connection status 87 | console.log(`Attempting to start session with args: ${JSON.stringify(args)}`); 88 | // Assuming callTool returns the 'result' part of the JSON-RPC response directly 89 | const result = await this.mcp.callTool({ name: "start", arguments: args }); // Use 'as any' for now to bypass strict type checking on result 90 | // Access content directly from the assumed 'result' payload 91 | const text = result?.content?.[0]?.text ?? ""; 92 | if (!text) { 93 | console.error("Raw start response object:", JSON.stringify(result, null, 2)); 94 | throw new Error(`Received empty or unexpected text content from 'start' tool.`); 95 | } 96 | const match = text.match(/Session (session-[0-9]+) started!/); 97 | if (!match || !match[1]) { 98 | console.error("Raw start response text:", text); 99 | throw new Error(`Failed to parse session ID from Deebo start output.`); 100 | } 101 | const sessionId = match[1]; 102 | console.log(`✅ Started session: ${sessionId}`); 103 | return sessionId; 104 | } 105 | async forceCheckSession(sessionId, maxRetries = 10) { 106 | this.ensureConnected(); // Check connection status 107 | const baseDelay = 1000; // Start with 1 second 108 | const maxDelay = 300000; // Cap at 5 minutes 109 | console.log(`Starting check loop for session ${sessionId} (Max ${maxRetries} attempts with exponential backoff)...`); 110 | for (let attempt = 1; attempt <= maxRetries; attempt++) { 111 | // Calculate delay with exponential backoff and jitter 112 | const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay); 113 | const jitter = delay * 0.1 * Math.random(); // Add 0-10% jitter 114 | const finalDelay = Math.floor(delay + jitter); 115 | console.log(`--- Check Attempt ${attempt}/${maxRetries} for ${sessionId} (delay: ${finalDelay}ms) ---`); 116 | let result; // Use 'any' type for result 117 | let text = ""; 118 | try { 119 | result = await this.mcp.callTool({ name: "check", arguments: { sessionId } }); // Use 'as any' 120 | // Access content directly from the assumed 'result' payload 121 | text = result?.content?.[0]?.text ?? ""; 122 | if (!text) { 123 | console.warn(`Attempt ${attempt}: Received empty or unexpected text content from 'check' tool.`); 124 | console.warn("Raw check response object:", JSON.stringify(result, null, 2)); 125 | } 126 | else { 127 | console.log(`Check attempt ${attempt} response snippet:\n${text.substring(0, 300)}...\n`); 128 | } 129 | } 130 | catch (checkError) { 131 | console.error(`Error during check attempt ${attempt}:`, checkError); 132 | text = `Error during check: ${checkError instanceof Error ? checkError.message : String(checkError)}`; 133 | } 134 | // Check for terminal statuses 135 | if (text.includes("Overall Status: completed") || text.includes("Overall Status: failed") || text.includes("Overall Status: cancelled")) { 136 | console.log(`✅ Session ${sessionId} reached terminal status on attempt ${attempt}.`); 137 | return text; 138 | } 139 | // Check for session not found 140 | if (text.includes(`Session ${sessionId} not found`)) { 141 | console.error(`Error: Session ${sessionId} reported as not found during check loop.`); 142 | throw new Error(`Session ${sessionId} not found during check.`); 143 | } 144 | // If not the last attempt, wait with exponential backoff 145 | if (attempt < maxRetries) { 146 | console.log(`Session not finished, waiting ${finalDelay}ms before next check...`); 147 | await new Promise((res) => setTimeout(res, finalDelay)); 148 | } 149 | else { 150 | console.error(`Session ${sessionId} did not finish after ${maxRetries} attempts.`); 151 | console.error(`Final check response text:\n${text}`); 152 | throw new Error(`Session ${sessionId} did not finish after ${maxRetries} attempts.`); 153 | } 154 | } 155 | throw new Error(`Unexpected exit from check loop for session ${sessionId}.`); 156 | } 157 | // Optional: AI analysis function 158 | async analyzeDeeboOutput(checkOutputText) { 159 | // ... (implementation remains the same) ... 160 | try { 161 | const prompt = `You are a CI assistant analyzing the final 'check' output of a Deebo debugging session. Based SOLELY on the following text, does this indicate a plausible final state for the session (completed, failed, cancelled)? Ignore transient errors mentioned in the output if a final status is present. Answer YES or NO and provide a brief one-sentence justification. 162 | 163 | Output to analyze: 164 | --- 165 | ${checkOutputText || "[No output provided]"} 166 | --- 167 | 168 | Analysis (YES/NO + Justification):`; 169 | const completion = await openrouterClient.chat.completions.create({ 170 | model: CI_LLM_MODEL, 171 | messages: [{ role: "user", content: prompt }], 172 | max_tokens: 100, 173 | temperature: 0.1, 174 | }); 175 | return completion.choices[0]?.message?.content?.trim() ?? "AI analysis failed."; 176 | } 177 | catch (error) { 178 | console.error("Error during AI analysis:", error); 179 | return `AI analysis step failed: ${error instanceof Error ? error.message : String(error)}`; 180 | } 181 | } 182 | async cleanup() { 183 | // Use internal flag instead of relying on potentially non-existent mcp.isConnected 184 | if (this.connected) { 185 | try { 186 | await this.mcp.close(); 187 | this.connected = false; 188 | console.log("CI Client Disconnected from Deebo Server."); 189 | } 190 | catch (closeError) { 191 | console.error("Error during client cleanup/close:", closeError); 192 | this.connected = false; // Ensure flag is set even if close fails 193 | } 194 | } 195 | else { 196 | // console.log("CI Client already disconnected or never connected.") 197 | } 198 | } 199 | } 200 | // --- Main Execution Logic --- 201 | async function main() { 202 | // Args: 203 | if (process.argv.length < 4) { 204 | console.error("Usage: node ci/mcp-client/build/index.js "); 205 | process.exit(1); 206 | } 207 | const deeboServerScriptPath = path.resolve(process.argv[2]); 208 | const repoFixturePathAbs = path.resolve(process.argv[3]); 209 | const startArgs = { 210 | "error": "Race condition in task cache management", 211 | "repoPath": repoFixturePathAbs, 212 | "language": "typescript", 213 | "filePath": path.join(repoFixturePathAbs, "src", "services", "taskService.ts"), 214 | "context": "// Cache the result - BUG: This is causing a race condition with invalidateTaskCache\n setCachedTasks(cacheKey, paginatedResponse)\n .catch(err => logger.error('Cache setting error:', err));\n\n return paginatedResponse;" 215 | }; 216 | const client = new MinimalMCPClient(); 217 | let exitCode = 0; 218 | let sessionId = ""; 219 | let finalCheckOutput = ""; 220 | try { 221 | console.log("--- Connecting Client to Server ---"); 222 | await client.connectToServer(deeboServerScriptPath); 223 | console.log("--- Forcing Start Session ---"); 224 | sessionId = await client.forceStartSession(startArgs); 225 | console.log("--- Forcing Check Session Loop ---"); 226 | finalCheckOutput = await client.forceCheckSession(sessionId); 227 | // Optional AI Analysis 228 | if (finalCheckOutput) { 229 | console.log("\n--- Requesting Optional AI Analysis of Final Check Output ---"); 230 | const analysisResult = await client.analyzeDeeboOutput(finalCheckOutput); 231 | console.log(analysisResult); 232 | } 233 | else { 234 | console.log("Skipping AI analysis as final check output was empty/error."); 235 | } 236 | console.log("--- Client Script Completed Successfully ---"); 237 | } 238 | catch (error) { 239 | console.error("--- Client Script Failed ---"); 240 | // Error should have been logged by the method that threw it 241 | exitCode = 1; 242 | } 243 | finally { 244 | await client.cleanup(); 245 | if (sessionId) { 246 | console.log(`FINAL_SESSION_ID_MARKER:${sessionId}`); 247 | } 248 | process.exit(exitCode); 249 | } 250 | } 251 | main(); 252 | -------------------------------------------------------------------------------- /ci/mcp-client/index.ts: -------------------------------------------------------------------------------- 1 | // ci/mcp-client/index.ts 2 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 3 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 4 | import OpenAI from "openai"; 5 | import dotenv from "dotenv"; 6 | import path from 'path'; 7 | 8 | dotenv.config(); 9 | 10 | const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY; 11 | const CI_LLM_MODEL = process.env.CI_LLM_MODEL || "deepseek/deepseek-chat"; // Keep for optional analysis 12 | 13 | if (!OPENROUTER_API_KEY) { 14 | throw new Error("OPENROUTER_API_KEY environment variable is not set"); 15 | } 16 | 17 | const openrouterClient = new OpenAI({ 18 | apiKey: OPENROUTER_API_KEY, 19 | baseURL: "https://openrouter.ai/api/v1", 20 | }); 21 | 22 | class MinimalMCPClient { 23 | private mcp: Client; 24 | private transport: StdioClientTransport | null = null; 25 | private connected: boolean = false; // Track connection status internally 26 | 27 | constructor() { 28 | this.mcp = new Client({ name: "deebo-ci-client", version: "1.0.0" }); 29 | } 30 | 31 | async connectToServer(deeboServerScriptPath: string) { 32 | if (this.connected) { 33 | console.log("CI Client already connected."); 34 | return; 35 | } 36 | if (!path.isAbsolute(deeboServerScriptPath)) { 37 | throw new Error(`Server script path must be absolute: ${deeboServerScriptPath}`); 38 | } 39 | try { 40 | // Ensure all required environment variables are present 41 | const requiredEnvVars = [ 42 | 'NODE_ENV', 43 | 'USE_MEMORY_BANK', 44 | 'MOTHER_HOST', 45 | 'MOTHER_MODEL', 46 | 'SCENARIO_HOST', 47 | 'SCENARIO_MODEL', 48 | 'OPENROUTER_API_KEY' 49 | ]; 50 | 51 | for (const envVar of requiredEnvVars) { 52 | if (!process.env[envVar]) { 53 | throw new Error(`Required environment variable ${envVar} is not set`); 54 | } 55 | } 56 | 57 | this.transport = new StdioClientTransport({ 58 | command: process.execPath, 59 | args: [ 60 | "--experimental-specifier-resolution=node", 61 | "--experimental-modules", 62 | "--max-old-space-size=4096", 63 | deeboServerScriptPath 64 | ], 65 | env: { 66 | ...process.env, // Inherit all environment variables from parent process 67 | // Explicitly set critical variables to ensure they're passed correctly 68 | NODE_ENV: process.env.NODE_ENV!, 69 | USE_MEMORY_BANK: process.env.USE_MEMORY_BANK!, 70 | MOTHER_HOST: process.env.MOTHER_HOST!, 71 | MOTHER_MODEL: process.env.MOTHER_MODEL!, 72 | SCENARIO_HOST: process.env.SCENARIO_HOST!, 73 | SCENARIO_MODEL: process.env.SCENARIO_MODEL!, 74 | OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY! 75 | } 76 | }); 77 | const connectPromise = this.mcp.connect(this.transport); 78 | const timeoutPromise = new Promise((_, reject) => 79 | setTimeout(() => reject(new Error("Connection timed out after 15 seconds")), 15000) 80 | ); 81 | await Promise.race([connectPromise, timeoutPromise]); 82 | this.connected = true; // Set connected flag 83 | console.log("CI Client Connected to Deebo Server"); 84 | } catch (e) { 85 | this.connected = false; // Ensure flag is false on error 86 | console.error("CI Client Failed to connect to MCP server: ", e); 87 | throw e; 88 | } 89 | } 90 | 91 | private ensureConnected() { 92 | if (!this.connected || !this.transport) { 93 | throw new Error("Client is not connected to the Deebo server."); 94 | } 95 | } 96 | 97 | 98 | async forceStartSession(args: Record): Promise { 99 | this.ensureConnected(); // Check connection status 100 | console.log(`Attempting to start session with args: ${JSON.stringify(args)}`); 101 | 102 | // Assuming callTool returns the 'result' part of the JSON-RPC response directly 103 | const result = await this.mcp.callTool({ name: "start", arguments: args }) as any; // Use 'as any' for now to bypass strict type checking on result 104 | 105 | // Access content directly from the assumed 'result' payload 106 | const text = result?.content?.[0]?.text ?? ""; 107 | if (!text) { 108 | console.error("Raw start response object:", JSON.stringify(result, null, 2)); 109 | throw new Error(`Received empty or unexpected text content from 'start' tool.`); 110 | } 111 | 112 | const match = text.match(/Session (session-[0-9]+) started!/); 113 | if (!match || !match[1]) { 114 | console.error("Raw start response text:", text); 115 | throw new Error(`Failed to parse session ID from Deebo start output.`); 116 | } 117 | const sessionId = match[1]; 118 | console.log(`✅ Started session: ${sessionId}`); 119 | return sessionId; 120 | } 121 | 122 | 123 | async forceCheckSession(sessionId: string, maxRetries = 10): Promise { 124 | this.ensureConnected(); // Check connection status 125 | 126 | const baseDelay = 1000; // Start with 1 second 127 | const maxDelay = 300000; // Cap at 5 minutes 128 | 129 | console.log(`Starting check loop for session ${sessionId} (Max ${maxRetries} attempts with exponential backoff)...`); 130 | for (let attempt = 1; attempt <= maxRetries; attempt++) { 131 | // Calculate delay with exponential backoff and jitter 132 | const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay); 133 | const jitter = delay * 0.1 * Math.random(); // Add 0-10% jitter 134 | const finalDelay = Math.floor(delay + jitter); 135 | 136 | console.log(`--- Check Attempt ${attempt}/${maxRetries} for ${sessionId} (delay: ${finalDelay}ms) ---`); 137 | let result : any; // Use 'any' type for result 138 | let text = ""; 139 | try { 140 | result = await this.mcp.callTool({ name: "check", arguments: { sessionId } }) as any; // Use 'as any' 141 | // Access content directly from the assumed 'result' payload 142 | text = result?.content?.[0]?.text ?? ""; 143 | if (!text) { 144 | console.warn(`Attempt ${attempt}: Received empty or unexpected text content from 'check' tool.`); 145 | console.warn("Raw check response object:", JSON.stringify(result, null, 2)); 146 | } else { 147 | console.log(`Check attempt ${attempt} response snippet:\n${text.substring(0, 300)}...\n`); 148 | } 149 | 150 | } catch (checkError) { 151 | console.error(`Error during check attempt ${attempt}:`, checkError); 152 | text = `Error during check: ${checkError instanceof Error ? checkError.message : String(checkError)}`; 153 | } 154 | 155 | // Check for terminal statuses 156 | if (text.includes("Overall Status: completed") || text.includes("Overall Status: failed") || text.includes("Overall Status: cancelled")) { 157 | console.log(`✅ Session ${sessionId} reached terminal status on attempt ${attempt}.`); 158 | return text; 159 | } 160 | 161 | // Check for session not found 162 | if (text.includes(`Session ${sessionId} not found`)) { 163 | console.error(`Error: Session ${sessionId} reported as not found during check loop.`); 164 | throw new Error(`Session ${sessionId} not found during check.`); 165 | } 166 | 167 | // If not the last attempt, wait with exponential backoff 168 | if (attempt < maxRetries) { 169 | console.log(`Session not finished, waiting ${finalDelay}ms before next check...`); 170 | await new Promise((res) => setTimeout(res, finalDelay)); 171 | } else { 172 | console.error(`Session ${sessionId} did not finish after ${maxRetries} attempts.`); 173 | console.error(`Final check response text:\n${text}`); 174 | throw new Error(`Session ${sessionId} did not finish after ${maxRetries} attempts.`); 175 | } 176 | } 177 | throw new Error(`Unexpected exit from check loop for session ${sessionId}.`); 178 | } 179 | 180 | 181 | // Optional: AI analysis function 182 | async analyzeDeeboOutput(checkOutputText: string): Promise { 183 | try { 184 | const prompt = `You are a CI assistant analyzing the final 'check' output of a Deebo debugging session. Provide a comprehensive analysis of the session based on the following output. Include: 185 | 186 | 1. Session Status: What was the final state (completed/failed/cancelled)? Was this expected? 187 | 2. Scenario Agents: How many were spawned? What were their key hypotheses? 188 | 3. Unusual Events: Were there any unexpected behaviors, errors, or interesting patterns? 189 | 4. Solution Quality: If completed, how thorough was the investigation? Were multiple approaches tried? 190 | 5. Performance: Any notable observations about execution time or resource usage? 191 | 192 | Keep your analysis technical and focused on actionable insights. 193 | 194 | Output to analyze: 195 | --- 196 | ${checkOutputText || "[No output provided]"} 197 | --- 198 | 199 | Analysis:`; 200 | 201 | const completion = await openrouterClient.chat.completions.create({ 202 | model: CI_LLM_MODEL, 203 | messages: [{ role: "user", content: prompt }], 204 | max_tokens: 500, 205 | temperature: 0.1, 206 | }); 207 | return completion.choices[0]?.message?.content?.trim() ?? "AI analysis failed."; 208 | 209 | } catch (error) { 210 | console.error("Error during AI analysis:", error); 211 | return `AI analysis step failed: ${error instanceof Error ? error.message : String(error)}`; 212 | } 213 | } 214 | 215 | 216 | async cleanup() { 217 | // Use internal flag instead of relying on potentially non-existent mcp.isConnected 218 | if (this.connected) { 219 | try { 220 | await this.mcp.close(); 221 | this.connected = false; 222 | console.log("CI Client Disconnected from Deebo Server."); 223 | } catch (closeError) { 224 | console.error("Error during client cleanup/close:", closeError); 225 | this.connected = false; // Ensure flag is set even if close fails 226 | } 227 | } else { 228 | // console.log("CI Client already disconnected or never connected.") 229 | } 230 | } 231 | } 232 | 233 | // --- Main Execution Logic --- 234 | async function main() { 235 | // Args: 236 | if (process.argv.length < 4) { 237 | console.error("Usage: node ci/mcp-client/build/index.js "); 238 | process.exit(1); 239 | } 240 | 241 | const deeboServerScriptPath = path.resolve(process.argv[2]); 242 | const repoFixturePathAbs = path.resolve(process.argv[3]); 243 | 244 | const startArgs = { 245 | "error": "Race condition in task cache management", 246 | "repoPath": repoFixturePathAbs, 247 | "language": "typescript", 248 | "filePath": path.join(repoFixturePathAbs, "src", "services", "taskService.ts"), 249 | "context": "// Cache the result - BUG: This is causing a race condition with invalidateTaskCache\n setCachedTasks(cacheKey, paginatedResponse)\n .catch(err => logger.error('Cache setting error:', err));\n\n return paginatedResponse;" 250 | }; 251 | 252 | const client = new MinimalMCPClient(); 253 | let exitCode = 0; 254 | let sessionId = ""; 255 | let finalCheckOutput = ""; 256 | 257 | try { 258 | console.log("--- Connecting Client to Server ---"); 259 | await client.connectToServer(deeboServerScriptPath); 260 | 261 | console.log("--- Forcing Start Session ---"); 262 | sessionId = await client.forceStartSession(startArgs); 263 | 264 | console.log("--- Forcing Check Session Loop ---"); 265 | finalCheckOutput = await client.forceCheckSession(sessionId); 266 | 267 | // Optional AI Analysis 268 | if (finalCheckOutput) { 269 | console.log("\n--- Requesting Optional AI Analysis of Final Check Output ---"); 270 | const analysisResult = await client.analyzeDeeboOutput(finalCheckOutput); 271 | console.log(analysisResult); 272 | } else { 273 | console.log("Skipping AI analysis as final check output was empty/error."); 274 | } 275 | 276 | console.log("--- Client Script Completed Successfully ---"); 277 | 278 | } catch (error) { 279 | console.error("--- Client Script Failed ---") 280 | // Error should have been logged by the method that threw it 281 | exitCode = 1; 282 | } finally { 283 | await client.cleanup(); 284 | if (sessionId) { 285 | console.log(`FINAL_SESSION_ID_MARKER:${sessionId}`); 286 | } 287 | process.exit(exitCode); 288 | } 289 | } 290 | 291 | main(); 292 | -------------------------------------------------------------------------------- /ci/mcp-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deebo-ci-client", 3 | "version": "1.0.0", 4 | "description": "Minimal MCP client for Deebo CI", 5 | "type": "module", 6 | "main": "build/index.js", 7 | "scripts": { 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "^1.7.0", 12 | "openai": "^4.91.1", 13 | "dotenv": "^16.4.7" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^22.13.14", 17 | "typescript": "^5.8.2" 18 | } 19 | } -------------------------------------------------------------------------------- /ci/mcp-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | // ci/mcp-client/tsconfig.json 2 | { 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "NodeNext", // Use NodeNext 6 | "moduleResolution": "NodeNext", // Use NodeNext 7 | "outDir": "./build", 8 | "rootDir": "./", // Compile files in this dir 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "resolveJsonModule": true // If you import JSON 14 | }, 15 | "include": ["index.ts"], // Only compile index.ts 16 | "exclude": ["node_modules"] 17 | } -------------------------------------------------------------------------------- /config/tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "tools": { 3 | "desktopCommander": { 4 | "command": "{npxPath}", 5 | "args": [ 6 | "@wonderwhy-er/desktop-commander" 7 | ], 8 | "env": { 9 | "RIPGREP_PATH": "{ripgrepPath}" 10 | } 11 | }, 12 | "git-mcp": { 13 | "command": "{uvxPath}", 14 | "args": [ 15 | "mcp-server-git", 16 | "--repository", 17 | "{repoPath}" 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /deebo_logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snagasuri/deebo-prototype/f01a29cf1263dfb51bfff18c75acb0c2907797a6/deebo_logo.jpeg -------------------------------------------------------------------------------- /gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | MODE=${1:-core} 4 | 5 | add_file_content() { 6 | if [ -f "$1" ]; then 7 | echo -e "\n=== $1 ===\n" >> core-files.txt 8 | cat "$1" >> core-files.txt 9 | else 10 | echo "Warning: File $1 not found, skipping." >&2 11 | fi 12 | } 13 | 14 | # Clear existing output file 15 | > core-files.txt 16 | 17 | echo "Generating core-files.txt in mode: $MODE" 18 | 19 | # Core source files 20 | add_file_content "src/index.ts" 21 | add_file_content "src/util/mcp.ts" 22 | add_file_content "config/tools.json" 23 | add_file_content "src/util/sanitize.ts" 24 | add_file_content "src/util/reports.ts" 25 | add_file_content "src/util/branch-manager.ts" 26 | add_file_content "src/util/agent-utils.ts" 27 | add_file_content "src/util/logger.ts" 28 | add_file_content "src/util/membank.ts" 29 | add_file_content "src/util/observations.ts" 30 | add_file_content "src/mother-agent.ts" 31 | add_file_content "src/scenario-agent.ts" 32 | 33 | # Only include packages if full mode is requested (just for deebo devs to look at installer stuff) 34 | if [ "$MODE" = "full" ]; then 35 | echo "Including package files..." 36 | 37 | add_file_content "packages/deebo-setup/src/utils.ts" 38 | add_file_content "packages/deebo-setup/src/types.ts" 39 | add_file_content "packages/deebo-setup/src/index.ts" 40 | add_file_content "packages/deebo-setup/package.json" 41 | 42 | add_file_content "packages/deebo-doctor/src/types.ts" 43 | add_file_content "packages/deebo-doctor/src/checks.ts" 44 | add_file_content "packages/deebo-doctor/src/index.ts" 45 | add_file_content "packages/deebo-doctor/package.json" 46 | fi 47 | 48 | # Config files 49 | add_file_content "package.json" 50 | add_file_content "tsconfig.json" 51 | add_file_content "README.md" 52 | 53 | echo "Done. Output written to core-files.txt." -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deebo-prototype", 3 | "version": "1.0.0", 4 | "main": "build/index.js", 5 | "bin": { 6 | "deebo": "build/index.js" 7 | }, 8 | "type": "module", 9 | "scripts": { 10 | "build": "tsc", 11 | "start": "node --experimental-specifier-resolution=node --experimental-modules --max-old-space-size=4096 build/index.js", 12 | "dev": "tsc --watch & node --experimental-specifier-resolution=node --experimental-modules --max-old-space-size=4096 --watch build/index.js" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "description": "Agentic Debugging System that integrates with Git and Filesystem MCP servers", 18 | "dependencies": { 19 | "@anthropic-ai/sdk": "^0.39.0", 20 | "@google/generative-ai": "^0.24.0", 21 | "@modelcontextprotocol/sdk": "^1.7.0", 22 | "@modelcontextprotocol/server-filesystem": "^2025.1.14", 23 | "@wonderwhy-er/desktop-commander": "^0.1.31", 24 | "cors": "^2.8.5", 25 | "dockerode": "^4.0.0", 26 | "dotenv": "^16.4.7", 27 | "express": "^5.0.1", 28 | "openai": "^4.91.1", 29 | "p-limit": "^6.2.0", 30 | "simple-git": "^3.27.0", 31 | "urlpattern-polyfill": "^10.0.0", 32 | "uuid": "^11.1.0", 33 | "zod": "^3.24.2" 34 | }, 35 | "optionalDependencies": { 36 | "@vscode/ripgrep": "^1.15.11" 37 | }, 38 | "devDependencies": { 39 | "@types/cors": "^2.8.17", 40 | "@types/dockerode": "^3.3.23", 41 | "@types/express": "^5.0.1", 42 | "@types/node": "^22.13.14", 43 | "@types/uuid": "^9.0.8", 44 | "typescript": "^5.8.2" 45 | }, 46 | "resolutions": { 47 | "@modelcontextprotocol/sdk": "1.7.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/deebo-doctor/build/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { homedir } from 'os'; 3 | import { join } from 'path'; 4 | import chalk from 'chalk'; 5 | import { allChecks } from './checks.js'; 6 | async function main() { 7 | // Parse arguments 8 | const verbose = process.argv.includes('--verbose'); 9 | const deeboPath = join(homedir(), '.deebo'); 10 | const logPath = verbose ? join(deeboPath, 'doctor.log') : undefined; 11 | const config = { 12 | verbose, 13 | deeboPath, 14 | logPath 15 | }; 16 | console.log(chalk.bold('\nDeebo Doctor - System Health Check\n')); 17 | // Run all checks 18 | let allPassed = true; 19 | for (const check of allChecks) { 20 | try { 21 | const result = await check.check(config); 22 | // Print result 23 | const icon = result.status === 'pass' ? '✔' : result.status === 'warn' ? '⚠' : '✖'; 24 | const color = result.status === 'pass' ? chalk.green : result.status === 'warn' ? chalk.yellow : chalk.red; 25 | console.log(color(`${icon} ${result.name}: ${result.message}`)); 26 | if (verbose && result.details) { 27 | console.log(chalk.dim(result.details)); 28 | } 29 | if (result.status === 'fail') { 30 | allPassed = false; 31 | } 32 | } 33 | catch (err) { 34 | console.error(chalk.red(`✖ ${check.name}: Error running check`)); 35 | if (verbose) { 36 | console.error(chalk.dim(err)); 37 | } 38 | allPassed = false; 39 | } 40 | } 41 | // Print summary 42 | console.log('\n' + (allPassed 43 | ? chalk.green('✔ All checks passed!') 44 | : chalk.red('✖ Some checks failed. Run with --verbose for more details.'))); 45 | process.exit(allPassed ? 0 : 1); 46 | } 47 | main().catch(err => { 48 | console.error(chalk.red('\n✖ Error running doctor:'), err); 49 | process.exit(1); 50 | }); 51 | -------------------------------------------------------------------------------- /packages/deebo-doctor/build/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /packages/deebo-doctor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deebo-doctor", 3 | "version": "1.0.19", 4 | "description": "Health check tool for Deebo debugging system", 5 | "type": "module", 6 | "bin": { 7 | "deebo-doctor": "build/index.js" 8 | }, 9 | "scripts": { 10 | "build": "tsc", 11 | "start": "node build/index.js" 12 | }, 13 | "dependencies": { 14 | "chalk": "^5.3.0", 15 | "inquirer": "^9.2.16", 16 | "simple-git": "^3.22.0", 17 | "zod": "^3.22.4" 18 | }, 19 | "devDependencies": { 20 | "@types/inquirer": "^9.0.7", 21 | "@types/node": "^20.11.28", 22 | "typescript": "^5.4.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/deebo-doctor/src/checks.ts: -------------------------------------------------------------------------------- 1 | import { CheckResult, DoctorConfig, SystemCheck } from './types.js'; 2 | import { homedir } from 'os'; 3 | import { join } from 'path'; 4 | import { access, readFile } from 'fs/promises'; 5 | import { simpleGit as createGit } from 'simple-git'; 6 | import { execSync } from 'child_process'; 7 | 8 | export const nodeVersionCheck: SystemCheck = { 9 | name: 'Node.js Version', 10 | async check() { 11 | const version = process.version; 12 | if (version.startsWith('v18') || version.startsWith('v20') || version.startsWith('v22')) { 13 | return { 14 | name: 'Node.js Version', 15 | status: 'pass', 16 | message: `Node ${version} detected`, 17 | }; 18 | } 19 | return { 20 | name: 'Node.js Version', 21 | status: 'fail', 22 | message: `Node.js v18+ required, found ${version}`, 23 | details: 'Install Node.js v18 or later from https://nodejs.org' 24 | }; 25 | } 26 | }; 27 | 28 | export const gitCheck: SystemCheck = { 29 | name: 'Git Installation', 30 | async check() { 31 | try { 32 | const git = createGit(); 33 | const version = await git.version(); 34 | return { 35 | name: 'Git Installation', 36 | status: 'pass', 37 | message: `Git ${version} detected`, 38 | }; 39 | } catch { 40 | return { 41 | name: 'Git Installation', 42 | status: 'fail', 43 | message: 'Git not found', 44 | details: 'Install Git from https://git-scm.com' 45 | }; 46 | } 47 | } 48 | }; 49 | 50 | export const mcpToolsCheck: SystemCheck = { 51 | name: 'MCP Tools', 52 | async check() { 53 | const results: CheckResult[] = []; 54 | 55 | // Check git-mcp 56 | try { 57 | const { execSync } = await import('child_process'); 58 | execSync('uvx mcp-server-git --help'); 59 | results.push({ 60 | name: 'git-mcp', 61 | status: 'pass', 62 | message: 'git-mcp installed' 63 | }); 64 | } catch { 65 | results.push({ 66 | name: 'git-mcp', 67 | status: 'fail', 68 | message: 'git-mcp not found', 69 | details: 'Install with: uvx mcp-server-git --help' 70 | }); 71 | } 72 | 73 | // Check desktop-commander 74 | if (process.platform === 'win32') { 75 | // On Windows, check for the .cmd shim which is required for proper stdin/stdout handling 76 | // Use actual npm prefix instead of assuming %APPDATA%\npm for nvm compatibility 77 | let cmdPath: string; 78 | try { 79 | const npmPrefix = execSync('npm config get prefix').toString().trim(); 80 | cmdPath = join(npmPrefix, 'desktop-commander.cmd'); 81 | } catch { 82 | // Fallback to traditional roaming path if npm command fails 83 | const base = process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'); 84 | cmdPath = join(base, 'npm', 'desktop-commander.cmd'); 85 | } 86 | try { 87 | await access(cmdPath); 88 | results.push({ 89 | name: 'desktop-commander', 90 | status: 'pass', 91 | message: 'desktop-commander.cmd found', 92 | details: `Path: ${cmdPath}` 93 | }); 94 | } catch { 95 | results.push({ 96 | name: 'desktop-commander', 97 | status: 'fail', 98 | message: 'desktop-commander.cmd not found', 99 | details: 'Install globally with: npm install -g @wonderwhy-er/desktop-commander' 100 | }); 101 | } 102 | } else { 103 | // On non-Windows, just check if it's installed 104 | try { 105 | const { execSync } = await import('child_process'); 106 | execSync('npx @wonderwhy-er/desktop-commander --help 2>/dev/null'); 107 | results.push({ 108 | name: 'desktop-commander', 109 | status: 'pass', 110 | message: 'desktop-commander installed' 111 | }); 112 | } catch { 113 | results.push({ 114 | name: 'desktop-commander', 115 | status: 'fail', 116 | message: 'desktop-commander not found', 117 | details: 'Install with: npx @wonderwhy-er/desktop-commander setup' 118 | }); 119 | } 120 | } 121 | 122 | // Aggregate results 123 | const allPass = results.every(r => r.status === 'pass'); 124 | return { 125 | name: 'MCP Tools', 126 | status: allPass ? 'pass' : 'fail', 127 | message: allPass ? 'All MCP tools installed' : 'Some MCP tools missing', 128 | details: results.map(r => `${r.name}: ${r.message}`).join('\n') 129 | }; 130 | } 131 | }; 132 | 133 | export const toolPathsCheck: SystemCheck = { 134 | name: 'Tool Paths', 135 | async check() { 136 | const results: CheckResult[] = []; 137 | const isWindows = process.platform === 'win32'; 138 | const { execSync } = await import('child_process'); 139 | 140 | // Check node 141 | try { 142 | const nodePath = execSync( 143 | isWindows ? 'cmd.exe /c where node.exe' : 'which node' 144 | ).toString().trim().split('\n')[0]; 145 | results.push({ 146 | name: 'node', 147 | status: 'pass', 148 | message: 'node found', 149 | details: `Path: ${nodePath}` 150 | }); 151 | } catch { 152 | results.push({ 153 | name: 'node', 154 | status: 'fail', 155 | message: 'node not found', 156 | details: 'Install Node.js from https://nodejs.org' 157 | }); 158 | } 159 | 160 | // Check npm 161 | try { 162 | const npmPath = execSync( 163 | isWindows ? 'cmd.exe /c where npm.cmd' : 'which npm' 164 | ).toString().trim().split('\n')[0]; 165 | results.push({ 166 | name: 'npm', 167 | status: 'pass', 168 | message: 'npm found', 169 | details: `Path: ${npmPath}` 170 | }); 171 | } catch { 172 | results.push({ 173 | name: 'npm', 174 | status: 'fail', 175 | message: 'npm not found', 176 | details: 'Install Node.js to get npm' 177 | }); 178 | } 179 | 180 | // Check npx 181 | try { 182 | const npxPath = execSync( 183 | isWindows ? 'cmd.exe /c where npx.cmd' : 'which npx' 184 | ).toString().trim().split('\n')[0]; 185 | results.push({ 186 | name: 'npx', 187 | status: 'pass', 188 | message: 'npx found', 189 | details: `Path: ${npxPath}` 190 | }); 191 | } catch { 192 | results.push({ 193 | name: 'npx', 194 | status: 'fail', 195 | message: 'npx not found', 196 | details: 'Install Node.js to get npx' 197 | }); 198 | } 199 | 200 | // Check uvx 201 | try { 202 | const uvxPath = execSync( 203 | isWindows ? 'cmd.exe /c where uvx.exe' : 'which uvx' 204 | ).toString().trim().split('\n')[0]; 205 | results.push({ 206 | name: 'uvx', 207 | status: 'pass', 208 | message: 'uvx found', 209 | details: `Path: ${uvxPath}` 210 | }); 211 | } catch { 212 | results.push({ 213 | name: 'uvx', 214 | status: 'fail', 215 | message: 'uvx not found', 216 | details: isWindows 217 | ? 'Run in PowerShell: irm https://astral.sh/uv/install.ps1 | iex' 218 | : 'Run: curl -LsSf https://astral.sh/uv/install.sh | sh' 219 | }); 220 | } 221 | 222 | // Check git 223 | try { 224 | const gitPath = execSync( 225 | isWindows ? 'cmd.exe /c where git.exe' : 'which git' 226 | ).toString().trim().split('\n')[0]; 227 | results.push({ 228 | name: 'git', 229 | status: 'pass', 230 | message: 'git found', 231 | details: `Path: ${gitPath}` 232 | }); 233 | } catch { 234 | results.push({ 235 | name: 'git', 236 | status: 'fail', 237 | message: 'git not found', 238 | details: 'Install git from https://git-scm.com' 239 | }); 240 | } 241 | 242 | // Check ripgrep 243 | try { 244 | const rgPath = execSync( 245 | isWindows ? 'cmd.exe /c where rg.exe' : 'which rg' 246 | ).toString().trim().split('\n')[0]; 247 | results.push({ 248 | name: 'ripgrep', 249 | status: 'pass', 250 | message: 'ripgrep found', 251 | details: `Path: ${rgPath}` 252 | }); 253 | } catch { 254 | results.push({ 255 | name: 'ripgrep', 256 | status: 'fail', 257 | message: 'ripgrep not found', 258 | details: isWindows 259 | ? 'Run in Command Prompt: winget install -e --id BurntSushi.ripgrep' 260 | : 'Run: brew install ripgrep' 261 | }); 262 | } 263 | 264 | // Check environment paths 265 | const pathEnv = process.env.PATH || ''; 266 | const pathSeparator = isWindows ? ';' : ':'; 267 | const paths = pathEnv.split(pathSeparator); 268 | 269 | // Common tool directories that should be in PATH 270 | const expectedPaths = isWindows 271 | ? ['\\npm', '\\git', '\\nodejs'] 272 | : ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin']; 273 | 274 | const missingPaths = expectedPaths.filter(expected => 275 | !paths.some(p => p.toLowerCase().includes(expected.toLowerCase())) 276 | ); 277 | 278 | if (missingPaths.length > 0) { 279 | results.push({ 280 | name: 'PATH', 281 | status: 'warn', 282 | message: 'Some expected paths missing from PATH', 283 | details: `Missing: ${missingPaths.join(', ')}\nCurrent PATH: ${pathEnv}` 284 | }); 285 | } else { 286 | results.push({ 287 | name: 'PATH', 288 | status: 'pass', 289 | message: 'All expected paths found in PATH', 290 | details: `PATH: ${pathEnv}` 291 | }); 292 | } 293 | 294 | // Aggregate results 295 | const allPass = results.every(r => r.status === 'pass'); 296 | const hasFails = results.some(r => r.status === 'fail'); 297 | return { 298 | name: 'Tool Paths', 299 | status: allPass ? 'pass' : hasFails ? 'fail' : 'warn', 300 | message: allPass 301 | ? 'All tool paths found' 302 | : hasFails 303 | ? 'Some required tools missing' 304 | : 'Tools found but some paths may need attention', 305 | details: results.map(r => `${r.name}: ${r.message}\n ${r.details || ''}`).join('\n\n') + `\n\nTroubleshooting Tips: 306 | 1. If deebo is failing at runtime (when starting a session), it's likely the system cannot find paths for MCP tools (git-mcp and desktopCommander). Run 'where uvx' to verify uvx is in your PATH. 307 | 308 | 2. If this check says "unable to find tool paths" even after installation: 309 | - Make sure to add the uvx/node paths to your environment 310 | - On Windows, check if uvx.exe is in your PATH by running 'where uvx' 311 | - Try closing and reopening your terminal to refresh environment variables 312 | 313 | 3. If deebo fails in the middle of a run after spawning scenario agents: 314 | - Don't worry! This is not a critical failure 315 | - You can always start a new deebo session 316 | - Tell it to look at its memory bank from the previous run 317 | - The memory bank contains all the progress and findings so far` 318 | }; 319 | } 320 | }; 321 | 322 | export const configFilesCheck: SystemCheck = { 323 | name: 'Configuration Files', 324 | async check(config: DoctorConfig) { 325 | const home = homedir(); 326 | const isWindows = process.platform === 'win32'; 327 | 328 | // Get VS Code and Cursor paths based on platform 329 | const vscodePath = isWindows 330 | ? join(process.env.APPDATA || '', 'Code', 'User', 'settings.json') 331 | : join(home, isWindows ? '' : 'Library/Application Support/Code/User/settings.json'); 332 | 333 | const cursorPath = isWindows 334 | ? join(process.env.APPDATA || '', '.cursor', 'mcp.json') 335 | : join(home, '.cursor', 'mcp.json'); 336 | 337 | const paths = isWindows ? { 338 | cline: join(process.env.APPDATA || '', 'Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), 339 | claude: join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json'), 340 | vscode: vscodePath, 341 | cursor: cursorPath, 342 | env: join(config.deeboPath, '.env'), 343 | tools: join(config.deeboPath, 'config/tools.json') 344 | } : { 345 | cline: join(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), 346 | claude: join(home, 'Library/Application Support/Claude/claude_desktop_config.json'), 347 | vscode: vscodePath, 348 | cursor: cursorPath, 349 | env: join(config.deeboPath, '.env'), 350 | tools: join(config.deeboPath, 'config/tools.json') 351 | }; 352 | 353 | const results: CheckResult[] = []; 354 | 355 | // Check each config file 356 | for (const [name, path] of Object.entries(paths)) { 357 | try { 358 | await access(path); 359 | const content = await readFile(path, 'utf8'); 360 | 361 | // Parse JSON if applicable 362 | if (name !== 'env') { 363 | const json = JSON.parse(content); 364 | 365 | // Check if Deebo is configured in MCP configs 366 | if ((name === 'cline' || name === 'claude') && (!json.mcpServers?.deebo)) { 367 | results.push({ 368 | name, 369 | status: 'fail', 370 | message: `${name} config exists but Deebo not configured`, 371 | details: `Path: ${path}\nAdd Deebo configuration to mcpServers` 372 | }); 373 | continue; 374 | } 375 | 376 | // Check tools.json structure 377 | if (name === 'tools' && (!json.tools?.desktopCommander || !json.tools?.['git-mcp'])) { 378 | results.push({ 379 | name, 380 | status: 'fail', 381 | message: `${name} config exists but missing required tools`, 382 | details: `Path: ${path}\nMissing one or more required tools: desktopCommander, git-mcp` 383 | }); 384 | continue; 385 | } 386 | } 387 | 388 | results.push({ 389 | name, 390 | status: 'pass', 391 | message: `${name} config found and valid`, 392 | details: `Path: ${path}` 393 | }); 394 | } catch { 395 | results.push({ 396 | name, 397 | status: 'fail', 398 | message: `${name} config not found or invalid`, 399 | details: `Expected at: ${path}` 400 | }); 401 | } 402 | } 403 | 404 | // Check if at least one MCP config is valid (cline, claude, vscode, or cursor) 405 | const mcpResults = results.filter(r => ['cline', 'claude', 'vscode', 'cursor'].includes(r.name)); 406 | const hasMcpConfig = mcpResults.some(r => r.status === 'pass'); 407 | 408 | // Check if core configs (env, tools) are valid 409 | const coreResults = results.filter(r => r.name === 'env' || r.name === 'tools'); 410 | const corePass = coreResults.every(r => r.status === 'pass'); 411 | 412 | return { 413 | name: 'Configuration Files', 414 | status: (hasMcpConfig && corePass) ? 'pass' : 'fail', 415 | message: hasMcpConfig ? 'All configuration files valid' : 'No valid MCP configuration found', 416 | details: results.map(r => `${r.name}: ${r.message}\n${r.details || ''}`).join('\n\n') 417 | }; 418 | } 419 | }; 420 | 421 | export const apiKeysCheck: SystemCheck = { 422 | name: 'API Keys', 423 | async check(config: DoctorConfig) { 424 | const envPath = join(config.deeboPath, '.env'); 425 | try { 426 | const content = await readFile(envPath, 'utf8'); 427 | const lines = content.split('\n'); 428 | const results: CheckResult[] = []; 429 | 430 | // Check each potential API key 431 | const keyChecks = { 432 | OPENROUTER_API_KEY: 'sk-or-v1-', 433 | OPENAI_API_KEY: 'sk-', 434 | ANTHROPIC_API_KEY: 'sk-ant-', 435 | GEMINI_API_KEY: 'AI' 436 | }; 437 | 438 | for (const [key, prefix] of Object.entries(keyChecks)) { 439 | const line = lines.find(l => l.startsWith(key)); 440 | if (!line) { 441 | results.push({ 442 | name: key, 443 | status: 'warn', 444 | message: `${key} not found` 445 | }); 446 | continue; 447 | } 448 | 449 | const value = line.split('=')[1]?.trim(); 450 | if (!value || !value.startsWith(prefix)) { 451 | results.push({ 452 | name: key, 453 | status: 'warn', 454 | message: `${key} may be invalid`, 455 | details: `Expected prefix: ${prefix}` 456 | }); 457 | continue; 458 | } 459 | 460 | results.push({ 461 | name: key, 462 | status: 'pass', 463 | message: `${key} found and valid` 464 | }); 465 | } 466 | 467 | // Aggregate results 468 | const allPass = results.some(r => r.status === 'pass'); 469 | return { 470 | name: 'API Keys', 471 | status: allPass ? 'pass' : 'warn', 472 | message: allPass ? 'At least one valid API key found' : 'No valid API keys found', 473 | details: results.map(r => `${r.name}: ${r.message}`).join('\n') 474 | }; 475 | } catch { 476 | return { 477 | name: 'API Keys', 478 | status: 'fail', 479 | message: 'Could not read .env file', 480 | details: `Expected at ${envPath}` 481 | }; 482 | } 483 | } 484 | }; 485 | 486 | export const guideServerCheck: SystemCheck = { 487 | name: 'Guide Server', 488 | async check(config: DoctorConfig) { 489 | const home = homedir(); 490 | const deeboGuidePath = join(home, '.deebo-guide'); // Changed to .deebo-guide 491 | const results: CheckResult[] = []; 492 | 493 | // Check guide server files 494 | const guidePath = join(deeboGuidePath, 'deebo_guide.md'); // Changed to deeboGuidePath 495 | const serverPath = join(deeboGuidePath, 'guide-server.js'); // Changed to deeboGuidePath 496 | 497 | try { 498 | await access(guidePath); 499 | results.push({ 500 | name: 'guide_file', 501 | status: 'pass', 502 | message: 'Deebo guide file found', 503 | details: `Path: ${guidePath}` 504 | }); 505 | } catch { 506 | results.push({ 507 | name: 'guide_file', 508 | status: 'fail', 509 | message: 'Deebo guide file not found', 510 | details: `Expected at: ${guidePath}` 511 | }); 512 | } 513 | 514 | try { 515 | await access(serverPath); 516 | results.push({ 517 | name: 'server_file', 518 | status: 'pass', 519 | message: 'Guide server file found', 520 | details: `Path: ${serverPath}` 521 | }); 522 | } catch { 523 | results.push({ 524 | name: 'server_file', 525 | status: 'fail', 526 | message: 'Guide server file not found', 527 | details: `Expected at: ${serverPath}` 528 | }); 529 | } 530 | 531 | // Check MCP configurations 532 | const isWindows = process.platform === 'win32'; 533 | const configPaths = isWindows ? { 534 | cline: join(process.env.APPDATA || '', 'Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), 535 | claude: join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json'), 536 | cursor: join(process.env.APPDATA || '', '.cursor/mcp.json') 537 | } : { 538 | cline: join(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), 539 | claude: join(home, 'Library/Application Support/Claude/claude_desktop_config.json'), 540 | cursor: join(home, '.cursor/mcp.json') 541 | }; 542 | 543 | for (const [name, path] of Object.entries(configPaths)) { 544 | try { 545 | const content = await readFile(path, 'utf8'); 546 | const config = JSON.parse(content); 547 | const guideServer = config.mcpServers?.['deebo-guide']; 548 | 549 | if (!guideServer) { 550 | results.push({ 551 | name: `${name}_config`, 552 | status: 'fail', 553 | message: `Guide server not configured in ${name}`, 554 | details: `Path: ${path}\nMissing 'deebo-guide' in mcpServers` 555 | }); 556 | continue; 557 | } 558 | 559 | results.push({ 560 | name: `${name}_config`, 561 | status: 'pass', 562 | message: `Guide server properly configured in ${name}`, 563 | details: `Path: ${path}` 564 | }); 565 | } catch { 566 | results.push({ 567 | name: `${name}_config`, 568 | status: 'fail', 569 | message: `Could not read ${name} config`, 570 | details: `Expected at: ${path}` 571 | }); 572 | } 573 | } 574 | 575 | // Aggregate results 576 | const allPass = results.every(r => r.status === 'pass'); 577 | const hasFails = results.some(r => r.status === 'fail'); 578 | 579 | return { 580 | name: 'Guide Server', 581 | status: allPass ? 'pass' : hasFails ? 'fail' : 'warn', 582 | message: allPass 583 | ? 'Guide server files and configuration valid' 584 | : 'Guide server setup incomplete', 585 | details: results.map(r => `${r.name}: ${r.message}\n${r.details || ''}`).join('\n\n') 586 | }; 587 | } 588 | }; 589 | 590 | export const allChecks = [ 591 | nodeVersionCheck, 592 | gitCheck, 593 | toolPathsCheck, 594 | mcpToolsCheck, 595 | configFilesCheck, 596 | apiKeysCheck, 597 | guideServerCheck 598 | ]; 599 | -------------------------------------------------------------------------------- /packages/deebo-doctor/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { homedir } from 'os'; 3 | import { join } from 'path'; 4 | import chalk from 'chalk'; 5 | import { allChecks } from './checks.js'; 6 | import { DoctorConfig } from './types.js'; 7 | 8 | async function main() { 9 | // Parse arguments 10 | const verbose = process.argv.includes('--verbose'); 11 | const deeboPath = join(homedir(), '.deebo'); 12 | const logPath = verbose ? join(deeboPath, 'doctor.log') : undefined; 13 | 14 | const config: DoctorConfig = { 15 | verbose, 16 | deeboPath, 17 | logPath 18 | }; 19 | 20 | console.log(chalk.bold('\nDeebo Doctor - System Health Check\n')); 21 | 22 | // Run all checks 23 | let allPassed = true; 24 | for (const check of allChecks) { 25 | try { 26 | const result = await check.check(config); 27 | 28 | // Print result 29 | const icon = result.status === 'pass' ? '✔' : result.status === 'warn' ? '⚠' : '✖'; 30 | const color = result.status === 'pass' ? chalk.green : result.status === 'warn' ? chalk.yellow : chalk.red; 31 | 32 | console.log(color(`${icon} ${result.name}: ${result.message}`)); 33 | 34 | if (verbose && result.details) { 35 | console.log(chalk.dim(result.details)); 36 | } 37 | 38 | if (result.status === 'fail') { 39 | allPassed = false; 40 | } 41 | } catch (err) { 42 | console.error(chalk.red(`✖ ${check.name}: Error running check`)); 43 | if (verbose) { 44 | console.error(chalk.dim(err)); 45 | } 46 | allPassed = false; 47 | } 48 | } 49 | 50 | // Print summary 51 | console.log('\n' + (allPassed 52 | ? chalk.green('✔ All checks passed!') 53 | : chalk.red('✖ Some checks failed. Run with --verbose for more details.'))); 54 | 55 | process.exit(allPassed ? 0 : 1); 56 | } 57 | 58 | main().catch(err => { 59 | console.error(chalk.red('\n✖ Error running doctor:'), err); 60 | process.exit(1); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/deebo-doctor/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface CheckResult { 2 | name: string; 3 | status: 'pass' | 'fail' | 'warn'; 4 | message: string; 5 | details?: string; 6 | } 7 | 8 | export interface DoctorConfig { 9 | verbose: boolean; 10 | deeboPath: string; 11 | logPath?: string; 12 | } 13 | 14 | export interface SystemCheck { 15 | name: string; 16 | check: (config: DoctorConfig) => Promise; 17 | fix?: (config: DoctorConfig) => Promise; 18 | } 19 | -------------------------------------------------------------------------------- /packages/deebo-doctor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "build"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/deebo-setup/build/deebo_guide.md: -------------------------------------------------------------------------------- 1 | ## Deebo Guide (For AI Assistants) 2 | 3 | **Agent Instructions:** Read this guide to understand Deebo and effectively assist your user with its installation, usage, and troubleshooting. Use the information here to answer user questions and guide them through processes. 4 | 5 | **1. What is Deebo? (Explain to the User)** 6 | 7 | * Inform the user that Deebo is an **autonomous debugging tool** you (the AI assistant) can delegate tasks to when encountering complex bugs. 8 | * Explain its function: Deebo receives bug details, generates multiple hypotheses, and tests them safely and concurrently in separate Git branches within the user's specified repository. 9 | * Highlight the benefit: It acts like a specialized team running parallel experiments to find the root cause or validate a fix more quickly. 10 | 11 | **2. Installation (Guide the User)** 12 | 13 | * Instruct the user to open their terminal and run: 14 | ```bash 15 | npx deebo-setup@latest 16 | ``` 17 | * Inform the user they will be prompted to choose AI models (LLMs) and provide API keys. 18 | * State the prerequisites: **Node.js (v18 or higher)** and **Git** must be installed and accessible in their system's PATH. 19 | * **Crucially:** After setup completes, strongly advise the user to verify the installation by running: 20 | ```bash 21 | npx deebo-doctor@latest 22 | ``` 23 | * Optionally, mention they can help the project by pinging usage stats (no private data sent): `npx deebo-setup ping` 24 | 25 | **3. How Deebo is Configured (Explain to the User)** 26 | 27 | * Clarify that `npx deebo-setup` configures Deebo **directly within the user's MCP client application** (e.g., VS Code `settings.json`, Cline `cline_mcp_settings.json`, Claude Desktop `claude_desktop_config.json`). 28 | * Explain that this client configuration includes the necessary commands to run Deebo, the chosen LLM hosts/models, and the API keys provided during setup. 29 | * Inform the user that when *you* (the AI agent) call a Deebo tool, the client uses this configuration to launch the Deebo server process with the correct settings (passed via environment variables from the client's MCP definition for Deebo). *Do not* refer the user to `~/.deebo/.env` for client integration configuration. 30 | 31 | **4. Core Usage: Deebo Tools (How You Interact)** 32 | 33 | You interact with Deebo using these MCP tools. Ensure you provide the correct arguments when calling them: 34 | 35 | * **`start` Tool:** 36 | 37 | * **Your Action:** Call this to begin a debugging investigation. 38 | * **Required Arguments:** 39 | * `error` (string): The error message or bug description from the user. 40 | * `repoPath` (string): The **absolute path** to the user's local Git repository. 41 | * **Optional Arguments:** 42 | * `context` (string): Any additional context provided by the user (code snippets, failed attempts, reproduction steps). Encourage the user to provide good context. 43 | * `language` (string): The primary programming language (e.g., 'typescript', 'python'). 44 | * `filePath` (string): The relative path within the repository to a specific relevant file, if known. 45 | * **Result:** You will receive a unique `sessionId` (e.g., `session-17xxxxxxxxxx`). Store this ID to use with other tools for this investigation. Inform the user of the session ID. 46 | 47 | * **`check` Tool:** 48 | 49 | * **Your Action:** Call this periodically to monitor an ongoing session. 50 | * **Required Argument:** `sessionId` (string). 51 | * **Result:** You receive a JSON object containing a text "pulse" report. Parse this report and relay the key information to the user (see Section 5). *Advise the user it may take \~30 seconds after starting for the first meaningful check report.* 52 | 53 | * **`cancel` Tool:** 54 | 55 | * **Your Action:** Call this if the user wants to stop an investigation or if it appears stuck. 56 | * **Required Argument:** `sessionId` (string). 57 | * **Result:** You receive confirmation that cancellation was requested. Inform the user. 58 | 59 | * **`add_observation` Tool:** 60 | 61 | * **Your Action:** Call this if the user provides a new hint or piece of information relevant to the ongoing investigation. 62 | * **Required Arguments:** `sessionId` (string), `observation` (string - the user's input). 63 | * **Optional Argument:** `agentId` (string, defaults to `'mother'`). Usually, send observations to the default 'mother' agent. 64 | * **Result:** You receive confirmation. Inform the user the observation was logged. 65 | 66 | **5. Understanding the `check` Report (Relay to User)** 67 | 68 | When you receive the `check` tool's response, parse the text report and clearly communicate these points to the user: 69 | 70 | * **Overall Status:** Is it `in_progress`, `completed`, `failed`, or `cancelled`? 71 | * **Duration:** How long has it been running? 72 | * **Mother Agent Info:** Relay its status and provide the `file://.../mother.log` link for detailed logs. If completed, summarize or provide the `` content if present. 73 | * **Scenario Agent Summaries:** For each scenario listed: 74 | * State its **Status** (`Running`, `Terminated`, `Reported`). 75 | * Mention the **Hypothesis** it was testing. 76 | * If `Reported`, summarize the **Outcome** (e.g., `CONFIRMED: Yes/No/Partially`) if available in the report snippet. 77 | * Provide the `file://` links to the scenario's detailed **log** and **report** files (usually in the session's `logs` and `reports` subdirectories). 78 | 79 | **6. Troubleshooting Steps (Guide the User)** 80 | 81 | If the user encounters problems, guide them through these steps: 82 | 83 | * **Step 1: Run Deebo Doctor:** 84 | 85 | * Instruct the user to run `npx deebo-doctor@latest` in their terminal. This checks common configuration and dependency issues. 86 | * If `deebo-doctor` reports failures or warnings, or if the issue persists, instruct them to run `npx deebo-doctor --verbose` and provide you (the agent) with the detailed output. 87 | * Inform the user that `deebo-doctor` verifies: 88 | * Correct Node.js and Git versions. 89 | * Correct paths and accessibility for `npx` and `uvx` commands. 90 | * Installation status of required MCP tools (`git-mcp` via `uvx`, `@wonderwhy-er/desktop-commander` via `npx`). 91 | * Existence and basic structure of Deebo configuration within their MCP client settings. 92 | 93 | * **Step 2: Address Specific Issues based on Doctor Output or Symptoms:** 94 | 95 | * **If `start` fails immediately:** 96 | * Ask the user to double-check that the `repoPath` provided is an **absolute path** and correct. 97 | * Check the `deebo-doctor --verbose` output for **Tool Paths** (`npx`, `uvx`). Ensure these commands work in the user's terminal. 98 | * Verify the Deebo installation path configured in the user's **client MCP settings** (under the `deebo` server definition) points to the correct location where Deebo was installed (usually `~/.deebo`). 99 | * Confirm `@wonderwhy-er/desktop-commander` is installed globally(`deebo-doctor` checks this). If missing, instruct the user: `npm install -g @wonderwhy-er/desktop-commander@latest`. 100 | * **If `check` returns "Session not found":** 101 | * Ask the user to confirm the `sessionId` is correct. 102 | * Explain the session might have finished or failed very quickly. Suggest checking the `~/.deebo/memory-bank/` directory structure for the relevant session folder and logs. 103 | * **If `check` shows "failed" status:** 104 | * Direct the user to examine the `mother.log` file (get the link from the `check` report). Look for specific error messages. 105 | * If errors mention LLMs or API keys, advise the user to verify the API keys stored in their **client's MCP configuration for Deebo** (these were set during `deebo-setup`). Also, suggest checking network connection, provider status, and account quotas. 106 | * **If errors mention `git-mcp` or `desktop-commander`:** 107 | * Refer to the `deebo-doctor` output under "MCP Tools". 108 | * If `git-mcp` issues are suspected, running `uvx mcp-server-git --help` might resolve path issues or confirm installation. 109 | * If `desktop-commander` issues are suspected, guide the user to run `npm install -g wonderwhy-er/desktop-commander`. 110 | 111 | **7. Best Practices (Advise the User)** 112 | 113 | * **Good Context is Key:** Provide detailed `context` when calling `start`, including error details, relevant code, and steps already tried. 114 | * **Monitor Progress:** Use `check` periodically rather than just waiting. 115 | * **Use Observations:** Explain that `add_observation` allows them to give Deebo hints during the investigation if they discover new information. 116 | * **Iterate:** If Deebo fails or gets stuck, perhaps use `cancel`, analyze the Session Pulse for insights, and start a new session with improved context. 117 | 118 | **8. Updating Deebo (Inform the User)** 119 | 120 | * To get the latest version, instruct the user to run: 121 | ```bash 122 | npx deebo-setup@latest 123 | ``` 124 | 125 | **9. Getting More Help (Provide Resources)** 126 | 127 | * If problems persist after following this guide and the `deebo-doctor` output, direct the user to: 128 | * Check the [Deebo GitHub Repository Issues](https://github.com/snagasuri/deebo-prototype/issues) for similar problems. 129 | * Open a new, detailed issue on the repository. 130 | * Contact the maintainer on X: [@sriramenn](https://www.google.com/search?q=https://x.com/sriramenn) 131 | -------------------------------------------------------------------------------- /packages/deebo-setup/build/guide-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { readFileSync } from 'fs'; 5 | import { join, dirname } from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | import { homedir } from 'os'; 8 | // Create server with explicit capabilities 9 | const server = new McpServer({ 10 | name: "deebo-guide", 11 | version: "1.0.0", 12 | capabilities: { 13 | tools: {}, 14 | resources: {}, 15 | prompts: {} 16 | } 17 | }); 18 | // Get guide path with reliable resolution 19 | const __dirname = dirname(fileURLToPath(import.meta.url)); 20 | const homeDir = homedir(); 21 | const deeboGuidePath = join(homeDir, '.deebo-guide'); 22 | const deeboPath = join(homeDir, '.deebo'); 23 | // Always use .deebo-guide for the guide file 24 | let guidePath = join(deeboGuidePath, 'deebo_guide.md'); 25 | // Register the guide tool with proper schema 26 | server.tool("read_deebo_guide", 27 | // Empty schema since this tool takes no parameters 28 | {}, async () => { 29 | try { 30 | const guide = readFileSync(guidePath, 'utf8'); 31 | return { 32 | content: [{ 33 | type: "text", 34 | text: guide 35 | }] 36 | }; 37 | } 38 | catch (error) { 39 | return { 40 | content: [{ 41 | type: "text", 42 | text: `Failed to read guide: ${error instanceof Error ? error.message : String(error)}` 43 | }], 44 | isError: true // Properly indicate error state 45 | }; 46 | } 47 | }); 48 | // Also expose the guide as a static resource 49 | server.resource("guide", "guide://deebo", async (uri) => { 50 | try { 51 | const guide = readFileSync(guidePath, 'utf8'); 52 | return { 53 | contents: [{ 54 | uri: uri.href, 55 | text: guide 56 | }] 57 | }; 58 | } 59 | catch (error) { 60 | throw new Error(`Failed to read guide: ${error instanceof Error ? error.message : String(error)}`); 61 | } 62 | }); 63 | // Connect server 64 | const transport = new StdioServerTransport(); 65 | await server.connect(transport); 66 | console.error("Deebo Guide MCP Server running"); 67 | -------------------------------------------------------------------------------- /packages/deebo-setup/build/guide-server/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { readFileSync } from 'fs'; 5 | import { join, dirname } from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | // Create server with explicit capabilities 8 | const server = new McpServer({ 9 | name: "deebo-guide", 10 | version: "1.0.0", 11 | capabilities: { 12 | tools: {}, 13 | resources: {}, 14 | prompts: {} 15 | } 16 | }); 17 | // Get guide path relative to this file 18 | const __dirname = dirname(fileURLToPath(import.meta.url)); 19 | const guidePath = join(__dirname, 'deebo_guide.md'); 20 | // Register the guide tool with proper schema 21 | server.tool("read_deebo_guide", 22 | // Empty schema since this tool takes no parameters 23 | {}, async () => { 24 | try { 25 | const guide = readFileSync(guidePath, 'utf8'); 26 | return { 27 | content: [{ 28 | type: "text", 29 | text: guide 30 | }] 31 | }; 32 | } 33 | catch (error) { 34 | return { 35 | content: [{ 36 | type: "text", 37 | text: `Failed to read guide: ${error instanceof Error ? error.message : String(error)}` 38 | }], 39 | isError: true // Properly indicate error state 40 | }; 41 | } 42 | }); 43 | // Also expose the guide as a static resource 44 | server.resource("guide", "guide://deebo", async (uri) => { 45 | try { 46 | const guide = readFileSync(guidePath, 'utf8'); 47 | return { 48 | contents: [{ 49 | uri: uri.href, 50 | text: guide 51 | }] 52 | }; 53 | } 54 | catch (error) { 55 | throw new Error(`Failed to read guide: ${error instanceof Error ? error.message : String(error)}`); 56 | } 57 | }); 58 | // Connect server 59 | const transport = new StdioServerTransport(); 60 | await server.connect(transport); 61 | console.error("Deebo Guide MCP Server running"); 62 | -------------------------------------------------------------------------------- /packages/deebo-setup/build/guide-setup.js: -------------------------------------------------------------------------------- 1 | import { homedir } from 'os'; 2 | import { join, dirname } from 'path'; 3 | import { copyFile, mkdir, readFile, writeFile } from 'fs/promises'; // Added copyFile 4 | import { fileURLToPath } from 'url'; 5 | import chalk from 'chalk'; 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | // Configure the guide server in an MCP client's config 8 | // Modified to accept guideServerScriptPath 9 | async function configureClientGuide(configPath, guideServerScriptPath) { 10 | try { 11 | let config = { mcpServers: {} }; 12 | try { 13 | config = JSON.parse(await readFile(configPath, 'utf8')); 14 | } 15 | catch { 16 | // File doesn't exist or is empty, use empty config 17 | } 18 | // Add guide server config without overwriting other servers 19 | config.mcpServers = { 20 | ...config.mcpServers, 21 | 'deebo-guide': { 22 | autoApprove: [], 23 | disabled: false, 24 | timeout: 30, 25 | command: 'node', 26 | args: [ 27 | guideServerScriptPath // Remove all experimental flags, they're not needed in Node.js v20+ 28 | ], 29 | env: { 30 | "NODE_ENV": "development" 31 | }, 32 | transportType: 'stdio' 33 | } 34 | }; 35 | // Create parent directory if needed 36 | await mkdir(dirname(configPath), { recursive: true }); 37 | // Write config file 38 | await writeFile(configPath, JSON.stringify(config, null, 2)); 39 | console.log(chalk.green(`✔ Added guide server to ${configPath}`)); 40 | } 41 | catch (err) { 42 | console.log(chalk.yellow(`⚠ Could not configure guide server in ${configPath}`)); 43 | console.log(chalk.dim(err instanceof Error ? err.message : String(err))); 44 | } 45 | } 46 | // Setup the guide server independently of main Deebo setup 47 | export async function setupGuideServer() { 48 | try { 49 | const home = homedir(); 50 | const deeboGuideUserDir = join(home, '.deebo-guide'); // Keep as .deebo-guide - this is intentional isolation 51 | await mkdir(deeboGuideUserDir, { recursive: true }); 52 | // Source paths (from npx package's build directory) 53 | const sourceGuideServerJsPath = join(__dirname, '../build/guide-server.js'); 54 | const sourceGuideMarkdownPath = join(__dirname, 'deebo_guide.md'); 55 | // Destination paths (in user's persistent .deebo-guide directory) 56 | const destGuideServerJsPath = join(deeboGuideUserDir, 'guide-server.js'); 57 | const destGuideMarkdownPath = join(deeboGuideUserDir, 'deebo_guide.md'); 58 | // Copy files to persistent location 59 | await copyFile(sourceGuideServerJsPath, destGuideServerJsPath); 60 | await copyFile(sourceGuideMarkdownPath, destGuideMarkdownPath); 61 | console.log(chalk.green('✔ Copied guide server files to persistent location.')); 62 | // Create or update package.json in .deebo-guide with required dependencies 63 | const packageJsonPath = join(deeboGuideUserDir, 'package.json'); 64 | const packageJson = { 65 | "type": "module", 66 | "dependencies": { 67 | "@modelcontextprotocol/sdk": "^1.0.0", 68 | "zod": "^3.22.4" 69 | } 70 | }; 71 | await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); 72 | console.log(chalk.green('✔ Created package.json with required dependencies.')); 73 | // Install dependencies in .deebo-guide directory 74 | try { 75 | const { execSync } = await import('child_process'); 76 | console.log(chalk.blue('Installing dependencies in .deebo-guide directory...')); 77 | execSync('npm install', { cwd: deeboGuideUserDir }); 78 | console.log(chalk.green('✔ Installed dependencies in .deebo-guide directory.')); 79 | } 80 | catch (err) { 81 | console.log(chalk.yellow(`⚠ Could not install dependencies in .deebo-guide: ${err instanceof Error ? err.message : String(err)}`)); 82 | console.log(chalk.yellow('Guide server may not function correctly without dependencies.')); 83 | } 84 | const platform = process.platform; 85 | const configPaths = {}; 86 | // Get paths based on platform 87 | if (platform === 'win32') { 88 | const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming'); 89 | configPaths.vscode = join(appData, 'Code', 'User', 'settings.json'); 90 | configPaths.cline = join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'); 91 | configPaths.claude = join(appData, 'Claude/claude_desktop_config.json'); 92 | configPaths.cursor = join(appData, '.cursor', 'mcp.json'); 93 | } 94 | else if (platform === 'linux') { 95 | configPaths.vscode = join(home, '.config', 'Code', 'User', 'settings.json'); 96 | configPaths.cline = join(home, '.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'); 97 | configPaths.claude = join(home, '.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/claude_desktop_config.json'); 98 | configPaths.cursor = join(home, '.cursor', 'mcp.json'); 99 | } 100 | else { 101 | // macOS 102 | configPaths.vscode = join(home, 'Library/Application Support/Code/User/settings.json'); 103 | configPaths.cline = join(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'); 104 | configPaths.claude = join(home, 'Library/Application Support/Claude/claude_desktop_config.json'); 105 | configPaths.cursor = join(home, '.cursor', 'mcp.json'); 106 | } 107 | // Configure in each client, passing the persistent path to the guide server script 108 | for (const [_client, clientConfigPath] of Object.entries(configPaths)) { 109 | await configureClientGuide(clientConfigPath, destGuideServerJsPath); 110 | } 111 | console.log(chalk.green('\n✔ Guide server setup complete!')); 112 | console.log(chalk.blue('AI assistants can now access Deebo guide even if main installation fails.')); 113 | } 114 | catch (error) { 115 | console.error(chalk.red('\n✖ Guide server setup failed:')); 116 | console.error(error instanceof Error ? error.message : String(error)); 117 | // Don't exit - let main setup continue even if guide setup fails 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/deebo-setup/build/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { homedir, platform as osPlatform } from 'os'; 3 | import { join } from 'path'; 4 | import inquirer from 'inquirer'; 5 | import chalk from 'chalk'; 6 | import { LlmHostSchema } from './types.js'; 7 | import { checkPrerequisites, findConfigPaths, setupDeeboDirectory, writeEnvFile, updateMcpConfig } from './utils.js'; 8 | import { setupGuideServer } from './guide-setup.js'; 9 | async function main() { 10 | try { 11 | // First, set up the guide server independently 12 | console.log(chalk.blue('\nSetting up Deebo Guide server...')); 13 | await setupGuideServer(); 14 | console.log(chalk.blue('\nProceeding with main Deebo installation...\n')); 15 | } 16 | catch (error) { 17 | console.error(chalk.yellow('\n⚠ Guide server setup encountered issues:')); 18 | console.error(chalk.dim(error instanceof Error ? error.message : String(error))); 19 | console.log(chalk.blue('Continuing with main installation...')); 20 | } 21 | // Check if this is a ping command 22 | if (process.argv.length > 2 && process.argv[2] === 'ping') { 23 | try { 24 | console.log(chalk.blue('Pinging Deebo installation tracker...')); 25 | // Simple ping with no extra dependencies or complexity 26 | const response = await fetch('https://deebo-active-counter.ramnag2003.workers.dev/ping', { 27 | method: 'POST', 28 | headers: { 'Content-Type': 'application/json' }, 29 | body: JSON.stringify({ hash: `user-${Date.now()}` }) 30 | }); 31 | if (response.ok) { 32 | console.log(chalk.green('✓ Successfully pinged Deebo installation tracker')); 33 | } 34 | else { 35 | console.log(chalk.yellow('⚠ Failed to ping installation tracker')); 36 | } 37 | } 38 | catch (error) { 39 | console.log(chalk.yellow('⚠ Could not reach installation tracker')); 40 | } 41 | return; 42 | } 43 | try { 44 | // Check prerequisites 45 | await checkPrerequisites(); 46 | // Find config paths 47 | const configPaths = await findConfigPaths(); 48 | // Get Mother agent configuration 49 | // Default models for mother agent 50 | const defaultModels = { 51 | openrouter: 'anthropic/claude-3.5-sonnet', 52 | openai: 'gpt-4o', 53 | anthropic: 'claude-3-5-sonnet-20241022', 54 | gemini: 'gemini-2.5-pro-preview-03-25' 55 | }; 56 | // Default models for scenario agents 57 | const scenarioDefaultModels = { 58 | openrouter: 'deepseek/deepseek-chat', 59 | openai: 'gpt-4o', 60 | anthropic: 'claude-3-5-sonnet-20241022', 61 | gemini: 'gemini-2.5-pro-preview-03-25' 62 | }; 63 | // Get Mother agent configuration 64 | const { motherHost } = await inquirer.prompt([{ 65 | type: 'list', 66 | name: 'motherHost', 67 | message: 'Choose LLM host for Mother agent:', 68 | choices: Object.keys(defaultModels) 69 | }]); 70 | const parsedMotherHost = LlmHostSchema.parse(motherHost); 71 | const { motherModel } = await inquirer.prompt([{ 72 | type: 'input', 73 | name: 'motherModel', 74 | message: `Enter model for Mother agent (press Enter for ${defaultModels[parsedMotherHost]}):`, 75 | default: defaultModels[parsedMotherHost] 76 | }]); 77 | // Get Scenario agent configuration 78 | const { scenarioHost } = await inquirer.prompt([{ 79 | type: 'list', 80 | name: 'scenarioHost', 81 | message: 'Choose LLM host for Scenario agents (press Enter to use same as Mother):', 82 | choices: Object.keys(defaultModels), 83 | default: parsedMotherHost 84 | }]); 85 | const parsedScenarioHost = LlmHostSchema.parse(scenarioHost); 86 | const { scenarioModel } = await inquirer.prompt([{ 87 | type: 'input', 88 | name: 'scenarioModel', 89 | message: `Enter model for Scenario agents (press Enter for ${scenarioDefaultModels[parsedScenarioHost]}):`, 90 | default: scenarioDefaultModels[parsedScenarioHost] 91 | }]); 92 | // Get API key 93 | const keyPrompt = parsedScenarioHost === parsedMotherHost 94 | ? `Enter your ${motherHost.toUpperCase()}_API_KEY:` 95 | : `Enter your ${motherHost.toUpperCase()}_API_KEY for Mother agent:`; 96 | const { motherApiKey } = await inquirer.prompt([{ 97 | type: 'password', 98 | name: 'motherApiKey', 99 | message: keyPrompt 100 | }]); 101 | // Show mother API key preview 102 | console.log(chalk.dim(`Mother API key preview: ${motherApiKey.substring(0, 8)}...`)); 103 | const { confirmMotherKey } = await inquirer.prompt([{ 104 | type: 'confirm', 105 | name: 'confirmMotherKey', 106 | message: 'Is this Mother API key correct?', 107 | default: true 108 | }]); 109 | if (!confirmMotherKey) { 110 | throw new Error('Mother API key confirmation failed. Please try again.'); 111 | } 112 | // Get Scenario agent API key if using different host 113 | let scenarioApiKey = motherApiKey; 114 | if (parsedScenarioHost !== parsedMotherHost) { 115 | const { useNewKey } = await inquirer.prompt([{ 116 | type: 'confirm', 117 | name: 'useNewKey', 118 | message: `Scenario agent uses different host (${scenarioHost}). Use different API key?`, 119 | default: true 120 | }]); 121 | if (useNewKey) { 122 | const { key } = await inquirer.prompt([{ 123 | type: 'password', 124 | name: 'key', 125 | message: `Enter your ${scenarioHost.toUpperCase()}_API_KEY for Scenario agents:` 126 | }]); 127 | // Show scenario API key preview 128 | console.log(chalk.dim(`Scenario API key preview: ${key.substring(0, 8)}...`)); 129 | const { confirmKey } = await inquirer.prompt([{ 130 | type: 'confirm', 131 | name: 'confirmKey', 132 | message: 'Is this Scenario API key correct?', 133 | default: true 134 | }]); 135 | if (!confirmKey) { 136 | throw new Error('Scenario API key confirmation failed. Please try again.'); 137 | } 138 | scenarioApiKey = key; 139 | } 140 | } 141 | // Setup paths 142 | const home = homedir(); 143 | const deeboPath = join(home, '.deebo'); 144 | const envPath = join(deeboPath, '.env'); 145 | // Create config object with cursorConfigPath 146 | const config = { 147 | deeboPath, 148 | envPath, 149 | motherHost: parsedMotherHost, 150 | motherModel, 151 | scenarioHost: parsedScenarioHost, 152 | scenarioModel, 153 | motherApiKey, 154 | scenarioApiKey, 155 | clineConfigPath: configPaths.cline, 156 | claudeConfigPath: configPaths.claude, 157 | vscodePath: configPaths.vscode 158 | }; 159 | // Ask about Cursor configuration 160 | const { useCursorGlobal } = await inquirer.prompt([{ 161 | type: 'confirm', 162 | name: 'useCursorGlobal', 163 | message: 'Configure Cursor globally (recommended for single-user systems)?', 164 | default: true 165 | }]); 166 | if (!useCursorGlobal) { 167 | console.log(chalk.blue('\nNote: Project-specific configuration will only apply to the selected directory.')); 168 | } 169 | if (useCursorGlobal) { 170 | // Use global Cursor config path 171 | const cursorPath = osPlatform() === 'win32' 172 | ? join(process.env.APPDATA || '', '.cursor') 173 | : join(home, '.cursor'); 174 | config.cursorConfigPath = join(cursorPath, 'mcp.json'); 175 | } 176 | else { 177 | // Let user select a directory for project-specific config 178 | const { projectPath } = await inquirer.prompt([{ 179 | type: 'input', 180 | name: 'projectPath', 181 | message: 'Enter path to project directory:', 182 | default: process.cwd() 183 | }]); 184 | config.cursorConfigPath = join(projectPath, '.cursor', 'mcp.json'); 185 | } 186 | console.log(chalk.blue('\nDetected installations:')); 187 | if (configPaths.cline) 188 | console.log('- Cline'); 189 | if (configPaths.claude) 190 | console.log('- Claude Desktop'); 191 | // Check if VS Code is actually installed 192 | try { 193 | const { execSync } = await import('child_process'); 194 | try { 195 | execSync('code --version', { stdio: 'ignore' }); 196 | console.log('- VS Code'); 197 | } 198 | catch { 199 | // VS Code not found 200 | } 201 | } 202 | catch { 203 | // Command execution failed 204 | } 205 | // Check if Cursor is actually installed 206 | try { 207 | const { execSync } = await import('child_process'); 208 | try { 209 | execSync('cursor --version', { stdio: 'ignore' }); 210 | console.log('- Cursor'); 211 | } 212 | catch { 213 | // Cursor not found 214 | } 215 | } 216 | catch { 217 | // Command execution failed 218 | } 219 | console.log(chalk.blue('\nNote for Windows users:')); 220 | console.log('You need to install DesktopCommander MCP globally by running: npm install -g @wonderwhy-er/desktop-commander'); 221 | // Setup Deebo 222 | await setupDeeboDirectory(config); 223 | await writeEnvFile(config); 224 | const { fullConfig, displayConfig } = await updateMcpConfig(config); 225 | // Display the config with masked API keys 226 | console.log(chalk.blue('\nMCP Configuration:')); 227 | console.log(chalk.dim('API keys are masked for security\n')); 228 | console.log(chalk.dim('Add this to your MCP settings:\n')); 229 | console.log(JSON.stringify(displayConfig, null, 2)); 230 | // Ask to copy full config 231 | const { copyConfig } = await inquirer.prompt([{ 232 | type: 'confirm', 233 | name: 'copyConfig', 234 | message: 'Copy full configuration to clipboard? (includes API keys)', 235 | default: false 236 | }]); 237 | if (copyConfig) { 238 | try { 239 | const { execSync } = await import('child_process'); 240 | const platform = process.platform; 241 | const configString = JSON.stringify(fullConfig, null, 2); 242 | if (platform === 'darwin') { 243 | execSync('pbcopy', { input: configString }); 244 | } 245 | else if (platform === 'win32') { 246 | execSync('clip', { input: configString }); 247 | } 248 | else { 249 | execSync('xclip -selection clipboard', { input: configString }); 250 | } 251 | console.log(chalk.green('✔ Configuration copied to clipboard')); 252 | } 253 | catch (err) { 254 | console.log(chalk.yellow('⚠ Failed to copy to clipboard')); 255 | console.log(chalk.dim(err instanceof Error ? err.message : String(err))); 256 | } 257 | } 258 | console.log(chalk.green('\n✔ Deebo installation complete!')); 259 | console.log(chalk.blue('\nNext steps:')); 260 | console.log('1. Restart your MCP client'); 261 | console.log('2. Run npx deebo-doctor to verify the installation (use --verbose for more details)'); 262 | } 263 | catch (error) { 264 | console.error(chalk.red('\n✖ Installation failed:')); 265 | console.error(error instanceof Error ? error.message : String(error)); 266 | process.exit(1); 267 | } 268 | } 269 | main(); 270 | -------------------------------------------------------------------------------- /packages/deebo-setup/build/types.js: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | export const LlmHostSchema = z.enum(['openrouter', 'anthropic', 'gemini', 'openai']); 3 | export const McpConfigSchema = z.object({ 4 | mcpServers: z.record(z.object({ 5 | autoApprove: z.array(z.string()), 6 | disabled: z.boolean(), 7 | timeout: z.number(), 8 | command: z.string(), 9 | args: z.array(z.string()), 10 | env: z.record(z.string()), 11 | transportType: z.string() 12 | })) 13 | }); 14 | export const LlmModelSchema = z.string(); 15 | -------------------------------------------------------------------------------- /packages/deebo-setup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deebo-setup", 3 | "version": "1.0.106", 4 | "description": "System installer for Deebo debugging tool", 5 | "type": "module", 6 | "bin": { 7 | "deebo-setup": "build/index.js" 8 | }, 9 | "exports": { 10 | "./guide-server": "./build/guide-server.js" 11 | }, 12 | "scripts": { 13 | "build": "tsc && npm run copy-files", 14 | "copy-files": "copyfiles -u 1 src/deebo_guide.md build/ && node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const { execSync } = require('child_process'); const homeDir = os.homedir(); const deeboPath = path.join(homeDir, '.deebo'); const guidePath = path.join(homeDir, '.deebo-guide'); fs.mkdirSync(guidePath, { recursive: true }); const srcPath = path.join(process.cwd(), 'build/guide-server.js'); const destDeeboPath = path.join(deeboPath, 'guide-server.js'); const destGuidePath = path.join(guidePath, 'guide-server.js'); fs.copyFileSync(srcPath, destDeeboPath); fs.copyFileSync(srcPath, destGuidePath); const packageJson = { type: 'module', dependencies: { '@modelcontextprotocol/sdk': '^1.0.0', 'zod': '^3.22.4' } }; fs.writeFileSync(path.join(guidePath, 'package.json'), JSON.stringify(packageJson, null, 2)); try { execSync('npm install', { cwd: guidePath }); console.log('Dependencies installed in .deebo-guide directory'); } catch (err) { console.error('Failed to install dependencies:', err); }\"", 15 | "start": "node build/index.js" 16 | }, 17 | "dependencies": { 18 | "chalk": "^5.3.0", 19 | "inquirer": "^9.2.16", 20 | "simple-git": "^3.22.0", 21 | "zod": "^3.22.4" 22 | }, 23 | "files": [ 24 | "build/**/*" 25 | ], 26 | "devDependencies": { 27 | "@types/inquirer": "^9.0.7", 28 | "@types/node": "^20.11.28", 29 | "copyfiles": "^2.4.1", 30 | "typescript": "^5.4.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/deebo-setup/src/deebo_guide.md: -------------------------------------------------------------------------------- 1 | ## Deebo Guide (For AI Assistants) 2 | 3 | **Agent Instructions:** Read this guide to understand Deebo and effectively assist your user with its installation, usage, and troubleshooting. Use the information here to answer user questions and guide them through processes. 4 | 5 | **1. What is Deebo? (Explain to the User)** 6 | 7 | * Inform the user that Deebo is an **autonomous debugging tool** you (the AI assistant) can delegate tasks to when encountering complex bugs. 8 | * Explain its function: Deebo receives bug details, generates multiple hypotheses, and tests them safely and concurrently in separate Git branches within the user's specified repository. 9 | * Highlight the benefit: It acts like a specialized team running parallel experiments to find the root cause or validate a fix more quickly. 10 | 11 | **2. Installation (Guide the User)** 12 | 13 | * Instruct the user to open their terminal and run: 14 | ```bash 15 | npx deebo-setup@latest 16 | ``` 17 | * Inform the user they will be prompted to choose AI models (LLMs) and provide API keys. 18 | * State the prerequisites: **Node.js (v18 or higher)** and **Git** must be installed and accessible in their system's PATH. 19 | * **Crucially:** After setup completes, strongly advise the user to verify the installation by running: 20 | ```bash 21 | npx deebo-doctor@latest 22 | ``` 23 | * Optionally, mention they can help the project by pinging usage stats (no private data sent): `npx deebo-setup ping` 24 | 25 | **3. How Deebo is Configured (Explain to the User)** 26 | 27 | * Clarify that `npx deebo-setup` configures Deebo **directly within the user's MCP client application** (e.g., VS Code `settings.json`, Cline `cline_mcp_settings.json`, Claude Desktop `claude_desktop_config.json`). 28 | * Explain that this client configuration includes the necessary commands to run Deebo, the chosen LLM hosts/models, and the API keys provided during setup. 29 | * Inform the user that when *you* (the AI agent) call a Deebo tool, the client uses this configuration to launch the Deebo server process with the correct settings (passed via environment variables from the client's MCP definition for Deebo). *Do not* refer the user to `~/.deebo/.env` for client integration configuration. 30 | 31 | **4. Core Usage: Deebo Tools (How You Interact)** 32 | 33 | You interact with Deebo using these MCP tools. Ensure you provide the correct arguments when calling them: 34 | 35 | * **`start` Tool:** 36 | 37 | * **Your Action:** Call this to begin a debugging investigation. 38 | * **Required Arguments:** 39 | * `error` (string): The error message or bug description from the user. 40 | * `repoPath` (string): The **absolute path** to the user's local Git repository. 41 | * **Optional Arguments:** 42 | * `context` (string): Any additional context provided by the user (code snippets, failed attempts, reproduction steps). Encourage the user to provide good context. 43 | * `language` (string): The primary programming language (e.g., 'typescript', 'python'). 44 | * `filePath` (string): The relative path within the repository to a specific relevant file, if known. 45 | * **Result:** You will receive a unique `sessionId` (e.g., `session-17xxxxxxxxxx`). Store this ID to use with other tools for this investigation. Inform the user of the session ID. 46 | 47 | * **`check` Tool:** 48 | 49 | * **Your Action:** Call this periodically to monitor an ongoing session. 50 | * **Required Argument:** `sessionId` (string). 51 | * **Result:** You receive a JSON object containing a text "pulse" report. Parse this report and relay the key information to the user (see Section 5). *Advise the user it may take \~30 seconds after starting for the first meaningful check report.* 52 | 53 | * **`cancel` Tool:** 54 | 55 | * **Your Action:** Call this if the user wants to stop an investigation or if it appears stuck. 56 | * **Required Argument:** `sessionId` (string). 57 | * **Result:** You receive confirmation that cancellation was requested. Inform the user. 58 | 59 | * **`add_observation` Tool:** 60 | 61 | * **Your Action:** Call this if the user provides a new hint or piece of information relevant to the ongoing investigation. 62 | * **Required Arguments:** `sessionId` (string), `observation` (string - the user's input). 63 | * **Optional Argument:** `agentId` (string, defaults to `'mother'`). Usually, send observations to the default 'mother' agent. 64 | * **Result:** You receive confirmation. Inform the user the observation was logged. 65 | 66 | **5. Understanding the `check` Report (Relay to User)** 67 | 68 | When you receive the `check` tool's response, parse the text report and clearly communicate these points to the user: 69 | 70 | * **Overall Status:** Is it `in_progress`, `completed`, `failed`, or `cancelled`? 71 | * **Duration:** How long has it been running? 72 | * **Mother Agent Info:** Relay its status and provide the `file://.../mother.log` link for detailed logs. If completed, summarize or provide the `` content if present. 73 | * **Scenario Agent Summaries:** For each scenario listed: 74 | * State its **Status** (`Running`, `Terminated`, `Reported`). 75 | * Mention the **Hypothesis** it was testing. 76 | * If `Reported`, summarize the **Outcome** (e.g., `CONFIRMED: Yes/No/Partially`) if available in the report snippet. 77 | * Provide the `file://` links to the scenario's detailed **log** and **report** files (usually in the session's `logs` and `reports` subdirectories). 78 | 79 | **6. Troubleshooting Steps (Guide the User)** 80 | 81 | If the user encounters problems, guide them through these steps: 82 | 83 | * **Step 1: Run Deebo Doctor:** 84 | 85 | * Instruct the user to run `npx deebo-doctor@latest` in their terminal. This checks common configuration and dependency issues. 86 | * If `deebo-doctor` reports failures or warnings, or if the issue persists, instruct them to run `npx deebo-doctor --verbose` and provide you (the agent) with the detailed output. 87 | * Inform the user that `deebo-doctor` verifies: 88 | * Correct Node.js and Git versions. 89 | * Correct paths and accessibility for `npx` and `uvx` commands. 90 | * Installation status of required MCP tools (`git-mcp` via `uvx`, `@wonderwhy-er/desktop-commander` via `npx`). 91 | * Existence and basic structure of Deebo configuration within their MCP client settings. 92 | 93 | * **Step 2: Address Specific Issues based on Doctor Output or Symptoms:** 94 | 95 | * **If `start` fails immediately:** 96 | * Ask the user to double-check that the `repoPath` provided is an **absolute path** and correct. 97 | * Check the `deebo-doctor --verbose` output for **Tool Paths** (`npx`, `uvx`). Ensure these commands work in the user's terminal. 98 | * Verify the Deebo installation path configured in the user's **client MCP settings** (under the `deebo` server definition) points to the correct location where Deebo was installed (usually `~/.deebo`). 99 | * Confirm `@wonderwhy-er/desktop-commander` is installed globally(`deebo-doctor` checks this). If missing, instruct the user: `npm install -g @wonderwhy-er/desktop-commander@latest`. 100 | * **If `check` returns "Session not found":** 101 | * Ask the user to confirm the `sessionId` is correct. 102 | * Explain the session might have finished or failed very quickly. Suggest checking the `~/.deebo/memory-bank/` directory structure for the relevant session folder and logs. 103 | * **If `check` shows "failed" status:** 104 | * Direct the user to examine the `mother.log` file (get the link from the `check` report). Look for specific error messages. 105 | * If errors mention LLMs or API keys, advise the user to verify the API keys stored in their **client's MCP configuration for Deebo** (these were set during `deebo-setup`). Also, suggest checking network connection, provider status, and account quotas. 106 | * **If errors mention `git-mcp` or `desktop-commander`:** 107 | * Refer to the `deebo-doctor` output under "MCP Tools". 108 | * If `git-mcp` issues are suspected, running `uvx mcp-server-git --help` might resolve path issues or confirm installation. 109 | * If `desktop-commander` issues are suspected, guide the user to run `npm install -g wonderwhy-er/desktop-commander`. 110 | 111 | **7. Best Practices (Advise the User)** 112 | 113 | * **Good Context is Key:** Provide detailed `context` when calling `start`, including error details, relevant code, and steps already tried. 114 | * **Monitor Progress:** Use `check` periodically rather than just waiting. 115 | * **Use Observations:** Explain that `add_observation` allows them to give Deebo hints during the investigation if they discover new information. 116 | * **Iterate:** If Deebo fails or gets stuck, perhaps use `cancel`, analyze the Session Pulse for insights, and start a new session with improved context. 117 | 118 | **8. Updating Deebo (Inform the User)** 119 | 120 | * To get the latest version, instruct the user to run: 121 | ```bash 122 | npx deebo-setup@latest 123 | ``` 124 | 125 | **9. Getting More Help (Provide Resources)** 126 | 127 | * If problems persist after following this guide and the `deebo-doctor` output, direct the user to: 128 | * Check the [Deebo GitHub Repository Issues](https://github.com/snagasuri/deebo-prototype/issues) for similar problems. 129 | * Open a new, detailed issue on the repository. 130 | * Contact the maintainer on X: [@sriramenn](https://www.google.com/search?q=https://x.com/sriramenn) 131 | -------------------------------------------------------------------------------- /packages/deebo-setup/src/guide-server.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { readFileSync } from 'fs'; 5 | import { join, dirname } from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | import { homedir } from 'os'; 8 | import { z } from "zod"; 9 | 10 | // Create server with explicit capabilities 11 | const server = new McpServer({ 12 | name: "deebo-guide", 13 | version: "1.0.0", 14 | capabilities: { 15 | tools: {}, 16 | resources: {}, 17 | prompts: {} 18 | } 19 | }); 20 | 21 | // Get guide path with reliable resolution 22 | const __dirname = dirname(fileURLToPath(import.meta.url)); 23 | const homeDir = homedir(); 24 | const deeboGuidePath = join(homeDir, '.deebo-guide'); 25 | const deeboPath = join(homeDir, '.deebo'); 26 | 27 | // Always use .deebo-guide for the guide file 28 | let guidePath = join(deeboGuidePath, 'deebo_guide.md'); 29 | 30 | // Register the guide tool with proper schema 31 | server.tool( 32 | "read_deebo_guide", 33 | // Empty schema since this tool takes no parameters 34 | {}, 35 | async () => { 36 | try { 37 | const guide = readFileSync(guidePath, 'utf8'); 38 | return { 39 | content: [{ 40 | type: "text", 41 | text: guide 42 | }] 43 | }; 44 | } catch (error) { 45 | return { 46 | content: [{ 47 | type: "text", 48 | text: `Failed to read guide: ${error instanceof Error ? error.message : String(error)}` 49 | }], 50 | isError: true // Properly indicate error state 51 | }; 52 | } 53 | } 54 | ); 55 | 56 | // Also expose the guide as a static resource 57 | server.resource( 58 | "guide", 59 | "guide://deebo", 60 | async (uri) => { 61 | try { 62 | const guide = readFileSync(guidePath, 'utf8'); 63 | return { 64 | contents: [{ 65 | uri: uri.href, 66 | text: guide 67 | }] 68 | }; 69 | } catch (error) { 70 | throw new Error(`Failed to read guide: ${error instanceof Error ? error.message : String(error)}`); 71 | } 72 | } 73 | ); 74 | 75 | // Connect server 76 | const transport = new StdioServerTransport(); 77 | await server.connect(transport); 78 | console.error("Deebo Guide MCP Server running"); 79 | -------------------------------------------------------------------------------- /packages/deebo-setup/src/guide-setup.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from 'os'; 2 | import { join, dirname } from 'path'; 3 | import { copyFile, mkdir, readFile, writeFile } from 'fs/promises'; // Added copyFile 4 | import { fileURLToPath } from 'url'; 5 | import chalk from 'chalk'; 6 | import { McpConfig } from './types.js'; 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)); 9 | 10 | // Configure the guide server in an MCP client's config 11 | // Modified to accept guideServerScriptPath 12 | async function configureClientGuide(configPath: string, guideServerScriptPath: string): Promise { 13 | try { 14 | let config: McpConfig = { mcpServers: {} }; 15 | try { 16 | config = JSON.parse(await readFile(configPath, 'utf8')) as McpConfig; 17 | } catch { 18 | // File doesn't exist or is empty, use empty config 19 | } 20 | 21 | // Add guide server config without overwriting other servers 22 | config.mcpServers = { 23 | ...config.mcpServers, 24 | 'deebo-guide': { 25 | autoApprove: [], 26 | disabled: false, 27 | timeout: 30, 28 | command: 'node', 29 | args: [ 30 | guideServerScriptPath // Remove all experimental flags, they're not needed in Node.js v20+ 31 | ], 32 | env: { 33 | "NODE_ENV": "development" 34 | }, 35 | transportType: 'stdio' 36 | } 37 | }; 38 | 39 | // Create parent directory if needed 40 | await mkdir(dirname(configPath), { recursive: true }); 41 | 42 | // Write config file 43 | await writeFile(configPath, JSON.stringify(config, null, 2)); 44 | console.log(chalk.green(`✔ Added guide server to ${configPath}`)); 45 | } catch (err) { 46 | console.log(chalk.yellow(`⚠ Could not configure guide server in ${configPath}`)); 47 | console.log(chalk.dim(err instanceof Error ? err.message : String(err))); 48 | } 49 | } 50 | 51 | // Setup the guide server independently of main Deebo setup 52 | export async function setupGuideServer(): Promise { 53 | try { 54 | const home = homedir(); 55 | const deeboGuideUserDir = join(home, '.deebo-guide'); // Keep as .deebo-guide - this is intentional isolation 56 | await mkdir(deeboGuideUserDir, { recursive: true }); 57 | 58 | // Source paths (from npx package's build directory) 59 | const sourceGuideServerJsPath = join(__dirname, '../build/guide-server.js'); 60 | const sourceGuideMarkdownPath = join(__dirname, 'deebo_guide.md'); 61 | 62 | // Destination paths (in user's persistent .deebo-guide directory) 63 | const destGuideServerJsPath = join(deeboGuideUserDir, 'guide-server.js'); 64 | const destGuideMarkdownPath = join(deeboGuideUserDir, 'deebo_guide.md'); 65 | 66 | // Copy files to persistent location 67 | await copyFile(sourceGuideServerJsPath, destGuideServerJsPath); 68 | await copyFile(sourceGuideMarkdownPath, destGuideMarkdownPath); 69 | console.log(chalk.green('✔ Copied guide server files to persistent location.')); 70 | 71 | // Create or update package.json in .deebo-guide with required dependencies 72 | const packageJsonPath = join(deeboGuideUserDir, 'package.json'); 73 | const packageJson = { 74 | "type": "module", 75 | "dependencies": { 76 | "@modelcontextprotocol/sdk": "^1.0.0", 77 | "zod": "^3.22.4" 78 | } 79 | }; 80 | 81 | await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); 82 | console.log(chalk.green('✔ Created package.json with required dependencies.')); 83 | 84 | // Install dependencies in .deebo-guide directory 85 | try { 86 | const { execSync } = await import('child_process'); 87 | console.log(chalk.blue('Installing dependencies in .deebo-guide directory...')); 88 | execSync('npm install', { cwd: deeboGuideUserDir }); 89 | console.log(chalk.green('✔ Installed dependencies in .deebo-guide directory.')); 90 | } catch (err) { 91 | console.log(chalk.yellow(`⚠ Could not install dependencies in .deebo-guide: ${err instanceof Error ? err.message : String(err)}`)); 92 | console.log(chalk.yellow('Guide server may not function correctly without dependencies.')); 93 | } 94 | 95 | const platform = process.platform; 96 | const configPaths: { [key: string]: string } = {}; 97 | 98 | // Get paths based on platform 99 | if (platform === 'win32') { 100 | const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming'); 101 | configPaths.vscode = join(appData, 'Code', 'User', 'settings.json'); 102 | configPaths.cline = join(appData, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'); 103 | configPaths.claude = join(appData, 'Claude/claude_desktop_config.json'); 104 | configPaths.cursor = join(appData, '.cursor', 'mcp.json'); 105 | } else if (platform === 'linux') { 106 | configPaths.vscode = join(home, '.config', 'Code', 'User', 'settings.json'); 107 | configPaths.cline = join(home, '.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'); 108 | configPaths.claude = join(home, '.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/claude_desktop_config.json'); 109 | configPaths.cursor = join(home, '.cursor', 'mcp.json'); 110 | } else { 111 | // macOS 112 | configPaths.vscode = join(home, 'Library/Application Support/Code/User/settings.json'); 113 | configPaths.cline = join(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'); 114 | configPaths.claude = join(home, 'Library/Application Support/Claude/claude_desktop_config.json'); 115 | configPaths.cursor = join(home, '.cursor', 'mcp.json'); 116 | } 117 | 118 | // Configure in each client, passing the persistent path to the guide server script 119 | for (const [_client, clientConfigPath] of Object.entries(configPaths)) { 120 | await configureClientGuide(clientConfigPath, destGuideServerJsPath); 121 | } 122 | 123 | console.log(chalk.green('\n✔ Guide server setup complete!')); 124 | console.log(chalk.blue('AI assistants can now access Deebo guide even if main installation fails.')); 125 | 126 | } catch (error) { 127 | console.error(chalk.red('\n✖ Guide server setup failed:')); 128 | console.error(error instanceof Error ? error.message : String(error)); 129 | // Don't exit - let main setup continue even if guide setup fails 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /packages/deebo-setup/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { homedir, platform as osPlatform } from 'os'; 3 | import { join } from 'path'; 4 | import inquirer from 'inquirer'; 5 | import chalk from 'chalk'; 6 | import { LlmHostSchema, LlmHost } from './types.js'; 7 | import { 8 | checkPrerequisites, 9 | findConfigPaths, 10 | setupDeeboDirectory, 11 | writeEnvFile, 12 | updateMcpConfig 13 | } from './utils.js'; 14 | import { setupGuideServer } from './guide-setup.js'; 15 | 16 | async function main() { 17 | try { 18 | // First, set up the guide server independently 19 | console.log(chalk.blue('\nSetting up Deebo Guide server...')); 20 | await setupGuideServer(); 21 | console.log(chalk.blue('\nProceeding with main Deebo installation...\n')); 22 | } catch (error) { 23 | console.error(chalk.yellow('\n⚠ Guide server setup encountered issues:')); 24 | console.error(chalk.dim(error instanceof Error ? error.message : String(error))); 25 | console.log(chalk.blue('Continuing with main installation...')); 26 | } 27 | 28 | // Check if this is a ping command 29 | if (process.argv.length > 2 && process.argv[2] === 'ping') { 30 | try { 31 | console.log(chalk.blue('Pinging Deebo installation tracker...')); 32 | // Simple ping with no extra dependencies or complexity 33 | const response = await fetch('https://deebo-active-counter.ramnag2003.workers.dev/ping', { 34 | method: 'POST', 35 | headers: { 'Content-Type': 'application/json' }, 36 | body: JSON.stringify({ hash: `user-${Date.now()}` }) 37 | }); 38 | 39 | if (response.ok) { 40 | console.log(chalk.green('✓ Successfully pinged Deebo installation tracker')); 41 | } else { 42 | console.log(chalk.yellow('⚠ Failed to ping installation tracker')); 43 | } 44 | } catch (error) { 45 | console.log(chalk.yellow('⚠ Could not reach installation tracker')); 46 | } 47 | return; 48 | } 49 | 50 | try { 51 | // Check prerequisites 52 | await checkPrerequisites(); 53 | 54 | // Find config paths 55 | const configPaths = await findConfigPaths(); 56 | 57 | // Get Mother agent configuration 58 | // Default models for mother agent 59 | const defaultModels: Record = { 60 | openrouter: 'anthropic/claude-3.5-sonnet', 61 | openai: 'gpt-4o', 62 | anthropic: 'claude-3-5-sonnet-20241022', 63 | gemini: 'gemini-2.5-pro-preview-03-25' 64 | }; 65 | 66 | // Default models for scenario agents 67 | const scenarioDefaultModels: Record = { 68 | openrouter: 'deepseek/deepseek-chat', 69 | openai: 'gpt-4o', 70 | anthropic: 'claude-3-5-sonnet-20241022', 71 | gemini: 'gemini-2.5-pro-preview-03-25' 72 | }; 73 | 74 | // Get Mother agent configuration 75 | const { motherHost } = await inquirer.prompt([{ 76 | type: 'list', 77 | name: 'motherHost', 78 | message: 'Choose LLM host for Mother agent:', 79 | choices: Object.keys(defaultModels) 80 | }]); 81 | 82 | const parsedMotherHost = LlmHostSchema.parse(motherHost); 83 | 84 | const { motherModel } = await inquirer.prompt([{ 85 | type: 'input', 86 | name: 'motherModel', 87 | message: `Enter model for Mother agent (press Enter for ${defaultModels[parsedMotherHost]}):`, 88 | default: defaultModels[parsedMotherHost] 89 | }]); 90 | 91 | // Get Scenario agent configuration 92 | const { scenarioHost } = await inquirer.prompt([{ 93 | type: 'list', 94 | name: 'scenarioHost', 95 | message: 'Choose LLM host for Scenario agents (press Enter to use same as Mother):', 96 | choices: Object.keys(defaultModels), 97 | default: parsedMotherHost 98 | }]); 99 | 100 | const parsedScenarioHost = LlmHostSchema.parse(scenarioHost); 101 | 102 | const { scenarioModel } = await inquirer.prompt([{ 103 | type: 'input', 104 | name: 'scenarioModel', 105 | message: `Enter model for Scenario agents (press Enter for ${scenarioDefaultModels[parsedScenarioHost]}):`, 106 | default: scenarioDefaultModels[parsedScenarioHost] 107 | }]); 108 | 109 | // Get API key 110 | const keyPrompt = parsedScenarioHost === parsedMotherHost 111 | ? `Enter your ${motherHost.toUpperCase()}_API_KEY:` 112 | : `Enter your ${motherHost.toUpperCase()}_API_KEY for Mother agent:`; 113 | 114 | const { motherApiKey } = await inquirer.prompt([{ 115 | type: 'password', 116 | name: 'motherApiKey', 117 | message: keyPrompt 118 | }]); 119 | 120 | // Show mother API key preview 121 | console.log(chalk.dim(`Mother API key preview: ${motherApiKey.substring(0, 8)}...`)); 122 | const { confirmMotherKey } = await inquirer.prompt([{ 123 | type: 'confirm', 124 | name: 'confirmMotherKey', 125 | message: 'Is this Mother API key correct?', 126 | default: true 127 | }]); 128 | 129 | if (!confirmMotherKey) { 130 | throw new Error('Mother API key confirmation failed. Please try again.'); 131 | } 132 | 133 | // Get Scenario agent API key if using different host 134 | let scenarioApiKey = motherApiKey; 135 | if (parsedScenarioHost !== parsedMotherHost) { 136 | const { useNewKey } = await inquirer.prompt([{ 137 | type: 'confirm', 138 | name: 'useNewKey', 139 | message: `Scenario agent uses different host (${scenarioHost}). Use different API key?`, 140 | default: true 141 | }]); 142 | 143 | if (useNewKey) { 144 | const { key } = await inquirer.prompt([{ 145 | type: 'password', 146 | name: 'key', 147 | message: `Enter your ${scenarioHost.toUpperCase()}_API_KEY for Scenario agents:` 148 | }]); 149 | 150 | // Show scenario API key preview 151 | console.log(chalk.dim(`Scenario API key preview: ${key.substring(0, 8)}...`)); 152 | const { confirmKey } = await inquirer.prompt([{ 153 | type: 'confirm', 154 | name: 'confirmKey', 155 | message: 'Is this Scenario API key correct?', 156 | default: true 157 | }]); 158 | 159 | if (!confirmKey) { 160 | throw new Error('Scenario API key confirmation failed. Please try again.'); 161 | } 162 | 163 | scenarioApiKey = key; 164 | } 165 | } 166 | 167 | // Setup paths 168 | const home = homedir(); 169 | const deeboPath = join(home, '.deebo'); 170 | const envPath = join(deeboPath, '.env'); 171 | 172 | // Create config object with cursorConfigPath 173 | const config: { 174 | deeboPath: string; 175 | envPath: string; 176 | motherHost: LlmHost; 177 | motherModel: string; 178 | scenarioHost: LlmHost; 179 | scenarioModel: string; 180 | motherApiKey: string; 181 | scenarioApiKey?: string; 182 | clineConfigPath?: string; 183 | claudeConfigPath?: string; 184 | vscodePath?: string; 185 | cursorConfigPath?: string; 186 | } = { 187 | deeboPath, 188 | envPath, 189 | motherHost: parsedMotherHost, 190 | motherModel, 191 | scenarioHost: parsedScenarioHost, 192 | scenarioModel, 193 | motherApiKey, 194 | scenarioApiKey, 195 | clineConfigPath: configPaths.cline, 196 | claudeConfigPath: configPaths.claude, 197 | vscodePath: configPaths.vscode 198 | }; 199 | 200 | // Ask about Cursor configuration 201 | const { useCursorGlobal } = await inquirer.prompt([{ 202 | type: 'confirm', 203 | name: 'useCursorGlobal', 204 | message: 'Configure Cursor globally (recommended for single-user systems)?', 205 | default: true 206 | }]); 207 | 208 | if (!useCursorGlobal) { 209 | console.log(chalk.blue('\nNote: Project-specific configuration will only apply to the selected directory.')); 210 | } 211 | 212 | if (useCursorGlobal) { 213 | // Use global Cursor config path 214 | const cursorPath = osPlatform() === 'win32' 215 | ? join(process.env.APPDATA || '', '.cursor') 216 | : join(home, '.cursor'); 217 | config.cursorConfigPath = join(cursorPath, 'mcp.json'); 218 | } else { 219 | // Let user select a directory for project-specific config 220 | const { projectPath } = await inquirer.prompt([{ 221 | type: 'input', 222 | name: 'projectPath', 223 | message: 'Enter path to project directory:', 224 | default: process.cwd() 225 | }]); 226 | config.cursorConfigPath = join(projectPath, '.cursor', 'mcp.json'); 227 | } 228 | 229 | console.log(chalk.blue('\nDetected installations:')); 230 | if (configPaths.cline) console.log('- Cline'); 231 | if (configPaths.claude) console.log('- Claude Desktop'); 232 | 233 | // Check if VS Code is actually installed 234 | try { 235 | const { execSync } = await import('child_process'); 236 | try { 237 | execSync('code --version', { stdio: 'ignore' }); 238 | console.log('- VS Code'); 239 | } catch { 240 | // VS Code not found 241 | } 242 | } catch { 243 | // Command execution failed 244 | } 245 | 246 | // Check if Cursor is actually installed 247 | try { 248 | const { execSync } = await import('child_process'); 249 | try { 250 | execSync('cursor --version', { stdio: 'ignore' }); 251 | console.log('- Cursor'); 252 | } catch { 253 | // Cursor not found 254 | } 255 | } catch { 256 | // Command execution failed 257 | } 258 | 259 | console.log(chalk.blue('\nNote for Windows users:')); 260 | console.log('You need to install DesktopCommander MCP globally by running: npm install -g @wonderwhy-er/desktop-commander'); 261 | 262 | // Setup Deebo 263 | await setupDeeboDirectory(config); 264 | await writeEnvFile(config); 265 | const { fullConfig, displayConfig } = await updateMcpConfig(config); 266 | 267 | // Display the config with masked API keys 268 | console.log(chalk.blue('\nMCP Configuration:')); 269 | console.log(chalk.dim('API keys are masked for security\n')); 270 | console.log(chalk.dim('Add this to your MCP settings:\n')); 271 | console.log(JSON.stringify(displayConfig, null, 2)); 272 | 273 | // Ask to copy full config 274 | const { copyConfig } = await inquirer.prompt([{ 275 | type: 'confirm', 276 | name: 'copyConfig', 277 | message: 'Copy full configuration to clipboard? (includes API keys)', 278 | default: false 279 | }]); 280 | 281 | if (copyConfig) { 282 | try { 283 | const { execSync } = await import('child_process'); 284 | const platform = process.platform; 285 | const configString = JSON.stringify(fullConfig, null, 2); 286 | 287 | if (platform === 'darwin') { 288 | execSync('pbcopy', { input: configString }); 289 | } else if (platform === 'win32') { 290 | execSync('clip', { input: configString }); 291 | } else { 292 | execSync('xclip -selection clipboard', { input: configString }); 293 | } 294 | console.log(chalk.green('✔ Configuration copied to clipboard')); 295 | } catch (err) { 296 | console.log(chalk.yellow('⚠ Failed to copy to clipboard')); 297 | console.log(chalk.dim(err instanceof Error ? err.message : String(err))); 298 | } 299 | } 300 | 301 | console.log(chalk.green('\n✔ Deebo installation complete!')); 302 | console.log(chalk.blue('\nNext steps:')); 303 | console.log('1. Restart your MCP client'); 304 | console.log('2. Run npx deebo-doctor to verify the installation (use --verbose for more details)'); 305 | 306 | } catch (error) { 307 | console.error(chalk.red('\n✖ Installation failed:')); 308 | console.error(error instanceof Error ? error.message : String(error)); 309 | process.exit(1); 310 | } 311 | } 312 | 313 | main(); 314 | -------------------------------------------------------------------------------- /packages/deebo-setup/src/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const LlmHostSchema = z.enum(['openrouter', 'anthropic', 'gemini', 'openai']); 4 | export type LlmHost = z.infer; 5 | 6 | export const McpConfigSchema = z.object({ 7 | mcpServers: z.record(z.object({ 8 | autoApprove: z.array(z.string()), 9 | disabled: z.boolean(), 10 | timeout: z.number(), 11 | command: z.string(), 12 | args: z.array(z.string()), 13 | env: z.record(z.string()), 14 | transportType: z.string() 15 | })) 16 | }); 17 | 18 | export type McpConfig = z.infer; 19 | 20 | export const LlmModelSchema = z.string(); 21 | export type LlmModel = z.infer; 22 | 23 | export interface SetupConfig { 24 | deeboPath: string; 25 | envPath: string; 26 | motherHost: LlmHost; 27 | motherModel: LlmModel; 28 | scenarioHost: LlmHost; 29 | scenarioModel: LlmModel; 30 | motherApiKey: string; 31 | scenarioApiKey?: string; // Optional - defaults to motherApiKey if hosts are same 32 | clineConfigPath?: string; 33 | claudeConfigPath?: string; 34 | vscodePath?: string; 35 | cursorConfigPath?: string; 36 | } 37 | -------------------------------------------------------------------------------- /packages/deebo-setup/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from 'os'; 2 | import { join, dirname } from 'path'; 3 | import { access, mkdir, readFile, writeFile } from 'fs/promises'; 4 | import { McpConfig, SetupConfig } from './types.js'; 5 | import chalk from 'chalk'; 6 | import { simpleGit as createGit } from 'simple-git'; 7 | import inquirer from 'inquirer'; 8 | 9 | export const DEEBO_REPO = 'https://github.com/snagasuri/deebo-prototype.git'; 10 | 11 | export async function checkPrerequisites(): Promise { 12 | // Check Node version 13 | const nodeVersion = process.version; 14 | const major = Number(nodeVersion.slice(1).split('.')[0]); 15 | 16 | if (major >= 18) { 17 | console.log(chalk.green('✔ Node version:', nodeVersion)); 18 | } else { 19 | throw new Error(`Node.js v18+ is required (found ${nodeVersion})`); 20 | } 21 | 22 | // Check git 23 | try { 24 | const git = createGit(); 25 | await git.version(); 26 | console.log(chalk.green('✔ git found')); 27 | } catch { 28 | throw new Error('git is required but not found'); 29 | } 30 | 31 | // Check ripgrep and install desktopCommander 32 | const platform = process.platform; 33 | try { 34 | const { execSync } = await import('child_process'); 35 | try { 36 | execSync('rg --version', { stdio: 'ignore' }); 37 | console.log(chalk.green('✔ ripgrep found')); 38 | } catch { 39 | console.log(chalk.yellow('⚠ ripgrep not found. Installing...')); 40 | 41 | switch(platform) { 42 | case 'win32': 43 | try { 44 | // Use cmd.exe with /c flag to execute winget in proper Windows shell context 45 | execSync('cmd.exe /c winget install -e --id BurntSushi.ripgrep', { 46 | stdio: 'inherit', 47 | windowsHide: true 48 | }); 49 | } catch { 50 | console.log(chalk.yellow('\nAutomatic ripgrep installation failed.')); 51 | console.log('Please install ripgrep manually using one of these methods:'); 52 | console.log('1. Download from: https://github.com/BurntSushi/ripgrep/releases'); 53 | console.log('2. Run in Command Prompt: winget install -e --id BurntSushi.ripgrep'); 54 | console.log('3. Run in PowerShell: scoop install ripgrep'); 55 | throw new Error('ripgrep installation required'); 56 | } 57 | break; 58 | case 'darwin': 59 | try { 60 | execSync('brew install ripgrep', { stdio: 'inherit' }); 61 | } catch { 62 | console.log(chalk.yellow('\nAutomatic ripgrep installation failed.')); 63 | console.log('Please install ripgrep manually:'); 64 | console.log('brew install ripgrep'); 65 | throw new Error('ripgrep installation required'); 66 | } 67 | break; 68 | default: 69 | console.log('Please install ripgrep using your system package manager:'); 70 | console.log('Ubuntu/Debian: sudo apt-get install ripgrep'); 71 | console.log('Fedora: sudo dnf install ripgrep'); 72 | console.log('Or visit: https://github.com/BurntSushi/ripgrep#installation'); 73 | throw new Error('ripgrep installation required'); 74 | } 75 | } 76 | } catch (error) { 77 | if (error instanceof Error && error.message === 'ripgrep installation required') { 78 | throw error; 79 | } 80 | console.log(chalk.yellow('⚠ Could not check for ripgrep')); 81 | throw new Error('Failed to check for ripgrep installation'); 82 | } 83 | 84 | // Check uvx 85 | try { 86 | const { execSync } = await import('child_process'); 87 | try { 88 | execSync('uvx --version', { stdio: 'ignore' }); 89 | console.log(chalk.green('✔ uvx found')); 90 | } catch { 91 | console.log(chalk.yellow('⚠ uvx not found. Installing...')); 92 | 93 | switch(platform) { 94 | case 'win32': 95 | try { 96 | execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', { 97 | stdio: 'inherit', 98 | windowsHide: true 99 | }); 100 | } catch { 101 | console.log(chalk.yellow('\nAutomatic uvx installation failed.')); 102 | console.log('Please install uvx manually:'); 103 | console.log('1. Run in PowerShell: irm https://astral.sh/uv/install.ps1 | iex'); 104 | console.log('2. Or visit: https://github.com/astral/uv#installation'); 105 | throw new Error('uvx installation required'); 106 | } 107 | break; 108 | case 'darwin': 109 | default: 110 | try { 111 | execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', { stdio: 'inherit' }); 112 | } catch { 113 | console.log(chalk.yellow('\nAutomatic uvx installation failed.')); 114 | console.log('Please install uvx manually:'); 115 | console.log('curl -LsSf https://astral.sh/uv/install.sh | sh'); 116 | console.log('Or visit: https://github.com/astral/uv#installation'); 117 | throw new Error('uvx installation required'); 118 | } 119 | } 120 | } 121 | } catch (error) { 122 | if (error instanceof Error && error.message === 'uvx installation required') { 123 | throw error; 124 | } 125 | console.log(chalk.yellow('⚠ Could not check for uvx')); 126 | throw new Error('Failed to check for uvx installation'); 127 | } 128 | 129 | // Install desktopCommander globally on Windows for proper .cmd shim 130 | if (platform === 'win32') { 131 | try { 132 | const { execSync } = await import('child_process'); 133 | try { 134 | execSync('npm install -g @wonderwhy-er/desktop-commander', { stdio: 'inherit' }); 135 | console.log(chalk.green('✔ Installed desktopCommander globally')); 136 | } catch { 137 | console.log(chalk.yellow('\nAutomatic desktopCommander installation failed.')); 138 | console.log('Please install manually: npm install -g @wonderwhy-er/desktop-commander'); 139 | } 140 | } catch (error) { 141 | console.log(chalk.yellow('⚠ Could not install desktopCommander')); 142 | } 143 | } 144 | } 145 | 146 | export async function findConfigPaths(): Promise<{ cline?: string; claude?: string; vscode?: string; cursor?: string }> { 147 | const home = homedir(); 148 | const platform = process.platform; 149 | 150 | // Get VS Code settings path based on platform 151 | let vscodePath: string; 152 | if (platform === 'win32') { 153 | // Use proper Windows default paths 154 | const appData = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'); 155 | vscodePath = join(appData, 'Code', 'User'); 156 | } else if (platform === 'linux') { 157 | vscodePath = join(home, '.config', 'Code', 'User'); 158 | } else { 159 | vscodePath = join(home, 'Library', 'Application Support', 'Code', 'User'); 160 | } 161 | 162 | // Create VS Code settings directory if it doesn't exist 163 | try { 164 | await mkdir(vscodePath, { recursive: true }); 165 | console.log(chalk.green('✔ Created VS Code settings directory')); 166 | } catch (err) { 167 | console.log(chalk.yellow('⚠ Could not create VS Code settings directory')); 168 | } 169 | 170 | type Paths = { cline: string; claude: string; vscode: string; cursor: string }; 171 | // Get Cursor path based on platform 172 | let cursorPath: string; 173 | if (platform === 'win32') { 174 | // Use proper Windows default paths 175 | const appData = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'); 176 | cursorPath = join(appData, '.cursor'); 177 | } else { 178 | cursorPath = join(home, '.cursor'); 179 | } 180 | 181 | // Create Cursor directory if it doesn't exist 182 | try { 183 | await mkdir(cursorPath, { recursive: true }); 184 | console.log(chalk.green('✔ Created Cursor settings directory')); 185 | } catch (err) { 186 | console.log(chalk.yellow('⚠ Could not create Cursor settings directory')); 187 | } 188 | 189 | let candidates: Paths[] = []; 190 | 191 | if (platform === 'win32') { 192 | // Standard VS Code 193 | candidates.push({ 194 | cline: join(process.env.APPDATA || '', 'Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), 195 | claude: join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json'), 196 | vscode: join(vscodePath, 'settings.json'), 197 | cursor: join(cursorPath, 'mcp.json') 198 | }); 199 | // VS Code Insiders 200 | candidates.push({ 201 | cline: join(process.env.APPDATA || '', 'Code - Insiders/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), 202 | claude: join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json'), 203 | vscode: join(vscodePath, 'settings.json'), 204 | cursor: join(cursorPath, 'mcp.json') 205 | }); 206 | } else if (platform === 'linux') { 207 | // Remote‐SSH / WSL 208 | candidates.push({ 209 | cline: join(home, '.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), 210 | claude: join(home, '.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/claude_desktop_config.json'), 211 | vscode: join(vscodePath, 'settings.json'), 212 | cursor: join(cursorPath, 'mcp.json') 213 | }); 214 | // Local VS Code 215 | candidates.push({ 216 | cline: join(home, '.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), 217 | claude: join(home, '.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/claude_desktop_config.json'), 218 | vscode: join(vscodePath, 'settings.json'), 219 | cursor: join(cursorPath, 'mcp.json') 220 | }); 221 | } else { 222 | // macOS 223 | candidates.push({ 224 | cline: join(home, 'Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json'), 225 | claude: join(home, 'Library/Application Support/Claude/claude_desktop_config.json'), 226 | vscode: join(vscodePath, 'settings.json'), 227 | cursor: join(cursorPath, 'mcp.json') 228 | }); 229 | } 230 | 231 | const result: { cline?: string; claude?: string; vscode?: string; cursor?: string } = { 232 | // Always include VS Code path since we created the directory 233 | vscode: join(vscodePath, 'settings.json') 234 | }; 235 | 236 | for (const { cline, claude, cursor } of candidates) { 237 | try { 238 | await access(cline); 239 | result.cline = cline; 240 | console.log(chalk.green(`✔ Cline config found at ${cline}`)); 241 | } catch { 242 | // not found here 243 | } 244 | 245 | try { 246 | await access(claude); 247 | result.claude = claude; 248 | console.log(chalk.green(`✔ Claude Desktop config found at ${claude}`)); 249 | } catch { 250 | // not found here 251 | } 252 | 253 | try { 254 | await access(cursor); 255 | result.cursor = cursor; 256 | console.log(chalk.green(`✔ Cursor config found at ${cursor}`)); 257 | } catch { 258 | // not found here 259 | } 260 | 261 | // stop as soon as we find something 262 | if (result.cline || result.claude || result.cursor) break; 263 | } 264 | 265 | 266 | return result; 267 | } 268 | 269 | export async function setupDeeboDirectory(config: SetupConfig): Promise { 270 | let needsCleanup = false; 271 | 272 | try { 273 | await access(config.deeboPath); 274 | // Directory exists, ask for confirmation 275 | const { confirm } = await inquirer.prompt([{ 276 | type: 'confirm', 277 | name: 'confirm', 278 | message: 'Deebo is already installed. Update to latest version?', 279 | default: true 280 | }]); 281 | 282 | if (!confirm) { 283 | console.log(chalk.yellow('Installation cancelled.')); 284 | process.exit(0); 285 | } 286 | 287 | needsCleanup = true; 288 | } catch (err) { 289 | // Directory doesn't exist, create it 290 | await mkdir(config.deeboPath, { recursive: true }); 291 | } 292 | 293 | // Clean up if needed 294 | if (needsCleanup) { 295 | const { rm } = await import('fs/promises'); 296 | await rm(config.deeboPath, { recursive: true, force: true }); 297 | console.log(chalk.green('✔ Removed existing installation')); 298 | await mkdir(config.deeboPath, { recursive: true }); 299 | } 300 | 301 | console.log(chalk.green('✔ Created Deebo directory')); 302 | 303 | // Clone repository 304 | const git = createGit(); 305 | await git.clone(DEEBO_REPO, config.deeboPath); 306 | console.log(chalk.green('✔ Cloned Deebo repository')); 307 | 308 | // Install dependencies 309 | const { execSync } = await import('child_process'); 310 | 311 | // On Windows, temporarily remove the preinstall script before installing 312 | if (process.platform === 'win32') { 313 | const pkgPath = join(config.deeboPath, 'package.json'); 314 | const pkg = JSON.parse(await readFile(pkgPath, 'utf8')); 315 | const originalPreinstall = pkg.scripts.preinstall; 316 | delete pkg.scripts.preinstall; 317 | await writeFile(pkgPath, JSON.stringify(pkg, null, 2)); 318 | 319 | try { 320 | execSync('npm install', { cwd: config.deeboPath, stdio: 'inherit' }); 321 | console.log(chalk.green('✔ Installed dependencies')); 322 | 323 | // Restore the preinstall script 324 | pkg.scripts.preinstall = originalPreinstall; 325 | await writeFile(pkgPath, JSON.stringify(pkg, null, 2)); 326 | } catch (err) { 327 | // Restore the preinstall script even if install fails 328 | pkg.scripts.preinstall = originalPreinstall; 329 | await writeFile(pkgPath, JSON.stringify(pkg, null, 2)); 330 | throw err; 331 | } 332 | } else { 333 | execSync('npm install', { cwd: config.deeboPath, stdio: 'inherit' }); 334 | console.log(chalk.green('✔ Installed dependencies')); 335 | } 336 | 337 | // Build project 338 | execSync('npm run build', { cwd: config.deeboPath, stdio: 'inherit' }); 339 | console.log(chalk.green('✔ Built project')); 340 | } 341 | 342 | export async function writeEnvFile(config: SetupConfig): Promise { 343 | let envContent = `MOTHER_HOST=${config.motherHost} 344 | MOTHER_MODEL=${config.motherModel} 345 | SCENARIO_HOST=${config.scenarioHost} 346 | SCENARIO_MODEL=${config.scenarioModel} 347 | ${getApiKeyEnvVar(config.motherHost)}=${config.motherApiKey}`; 348 | 349 | // Add scenario API key if different from mother 350 | if (config.scenarioHost !== config.motherHost && config.scenarioApiKey) { 351 | envContent += `\n${getApiKeyEnvVar(config.scenarioHost)}=${config.scenarioApiKey}`; 352 | } 353 | 354 | envContent += `\nUSE_MEMORY_BANK=true 355 | NODE_ENV=development`; 356 | 357 | await writeFile(config.envPath, envContent); 358 | console.log(chalk.green('✔ Created environment file')); 359 | } 360 | 361 | function maskApiKey(key: string): string { 362 | if (!key) return ''; 363 | const start = key.substring(0, 8); 364 | const end = key.substring(key.length - 4); 365 | return `${start}...${end}`; 366 | } 367 | 368 | function getDisplayConfig(config: SetupConfig, deeboConfig: any): any { 369 | const displayConfig = { 370 | deebo: JSON.parse(JSON.stringify(deeboConfig)) 371 | }; 372 | 373 | // Mask API keys in env 374 | const motherKeyVar = getApiKeyEnvVar(config.motherHost); 375 | if (displayConfig.deebo.env[motherKeyVar]) { 376 | displayConfig.deebo.env[motherKeyVar] = maskApiKey(config.motherApiKey); 377 | } 378 | 379 | if (config.scenarioHost !== config.motherHost && config.scenarioApiKey) { 380 | const scenarioKeyVar = getApiKeyEnvVar(config.scenarioHost); 381 | if (displayConfig.deebo.env[scenarioKeyVar]) { 382 | displayConfig.deebo.env[scenarioKeyVar] = maskApiKey(config.scenarioApiKey); 383 | } 384 | } 385 | 386 | return displayConfig; 387 | } 388 | 389 | export async function updateMcpConfig(config: SetupConfig): Promise<{ fullConfig: any; displayConfig: any }> { 390 | const serverConfig = { 391 | autoApprove: [], 392 | disabled: false, 393 | timeout: 30, 394 | command: 'node', 395 | args: [ 396 | '--experimental-specifier-resolution=node', 397 | '--experimental-modules', 398 | '--max-old-space-size=4096', 399 | join(config.deeboPath, 'build/index.js') 400 | ], 401 | env: { 402 | NODE_ENV: 'development', 403 | USE_MEMORY_BANK: 'true', 404 | MOTHER_HOST: config.motherHost, 405 | MOTHER_MODEL: config.motherModel, 406 | SCENARIO_HOST: config.scenarioHost, 407 | SCENARIO_MODEL: config.scenarioModel, 408 | [getApiKeyEnvVar(config.motherHost)]: config.motherApiKey, 409 | ...(config.scenarioHost !== config.motherHost && config.scenarioApiKey ? { 410 | [getApiKeyEnvVar(config.scenarioHost)]: config.scenarioApiKey 411 | } : {}) 412 | }, 413 | transportType: 'stdio' 414 | }; 415 | 416 | const fullConfig = { 417 | deebo: serverConfig 418 | }; 419 | 420 | // Create display config with masked API keys 421 | const displayConfig = getDisplayConfig(config, serverConfig); 422 | 423 | // Update Cline config if available 424 | if (config.clineConfigPath) { 425 | const clineConfig = JSON.parse(await readFile(config.clineConfigPath, 'utf8')) as McpConfig; 426 | clineConfig.mcpServers.deebo = serverConfig; 427 | await writeFile(config.clineConfigPath, JSON.stringify(clineConfig, null, 2)); 428 | console.log(chalk.green('✔ Updated Cline configuration')); 429 | } 430 | 431 | // Update Claude config if available 432 | if (config.claudeConfigPath) { 433 | const claudeConfig = JSON.parse(await readFile(config.claudeConfigPath, 'utf8')) as McpConfig; 434 | claudeConfig.mcpServers.deebo = serverConfig; 435 | await writeFile(config.claudeConfigPath, JSON.stringify(claudeConfig, null, 2)); 436 | console.log(chalk.green('✔ Updated Claude Desktop configuration')); 437 | } 438 | 439 | // Update Cursor config if available 440 | if (config.cursorConfigPath) { 441 | try { 442 | let cursorConfig: McpConfig = { mcpServers: {} }; 443 | try { 444 | // Try to read existing config 445 | cursorConfig = JSON.parse(await readFile(config.cursorConfigPath, 'utf8')) as McpConfig; 446 | } catch { 447 | // File doesn't exist or is empty, use empty config 448 | } 449 | 450 | // Add Deebo config without overwriting other servers 451 | cursorConfig.mcpServers = { 452 | ...cursorConfig.mcpServers, 453 | deebo: serverConfig 454 | }; 455 | 456 | // Create parent directory if it doesn't exist 457 | await mkdir(dirname(config.cursorConfigPath), { recursive: true }); 458 | 459 | // Write config file 460 | await writeFile(config.cursorConfigPath, JSON.stringify(cursorConfig, null, 2)); 461 | console.log(chalk.green('✔ Updated Cursor configuration')); 462 | console.log(chalk.dim(` Config file: ${config.cursorConfigPath}`)); 463 | } catch (err) { 464 | console.log(chalk.yellow('⚠ Could not update Cursor configuration')); 465 | console.log(chalk.dim(err instanceof Error ? err.message : String(err))); 466 | } 467 | } 468 | 469 | // Update VS Code settings if available 470 | if (config.vscodePath) { 471 | try { 472 | let settings = {}; 473 | try { 474 | settings = JSON.parse(await readFile(config.vscodePath, 'utf8')); 475 | } catch { 476 | // File doesn't exist or is empty, use empty object 477 | } 478 | 479 | // Add MCP settings 480 | const mcpSettings = settings as Record; 481 | mcpSettings.mcp = mcpSettings.mcp || {}; 482 | mcpSettings.mcp.servers = mcpSettings.mcp.servers || {}; 483 | mcpSettings.mcp.servers.deebo = serverConfig; 484 | mcpSettings['chat.mcp.enabled'] = true; 485 | 486 | // Create parent directory if it doesn't exist 487 | await mkdir(dirname(config.vscodePath), { recursive: true }); 488 | 489 | // Write settings file 490 | await writeFile(config.vscodePath, JSON.stringify(mcpSettings, null, 2)); 491 | console.log(chalk.green('✔ Updated VS Code settings')); 492 | console.log(chalk.dim(` Settings file: ${config.vscodePath}`)); 493 | } catch (err) { 494 | console.log(chalk.yellow('⚠ Could not update VS Code settings')); 495 | console.log(chalk.dim(err instanceof Error ? err.message : String(err))); 496 | } 497 | } 498 | 499 | return { fullConfig, displayConfig }; 500 | 501 | } 502 | 503 | function getDefaultModel(host: string): string { 504 | switch (host) { 505 | case 'openrouter': 506 | return 'anthropic/claude-3.5-sonnet'; 507 | case 'anthropic': 508 | return 'claude-3-sonnet-20240229'; 509 | case 'gemini': 510 | return 'gemini-2.5-pro-preview-03-25'; 511 | default: 512 | return 'anthropic/claude-3.5-sonnet'; 513 | } 514 | } 515 | 516 | function getApiKeyEnvVar(host: string): string { 517 | switch (host) { 518 | case 'openrouter': 519 | return 'OPENROUTER_API_KEY'; 520 | case 'openai': 521 | return 'OPENAI_API_KEY'; 522 | case 'anthropic': 523 | return 'ANTHROPIC_API_KEY'; 524 | case 'gemini': 525 | return 'GEMINI_API_KEY'; 526 | default: 527 | return 'OPENROUTER_API_KEY'; 528 | } 529 | } 530 | 531 | // Removed the pingInstallation function - implemented directly in index.ts 532 | -------------------------------------------------------------------------------- /packages/deebo-setup/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules", "build"] 15 | } 16 | -------------------------------------------------------------------------------- /src/scenario-agent.ts: -------------------------------------------------------------------------------- 1 | // src/scenario-agent.ts 2 | 3 | import { log } from './util/logger.js'; 4 | import { connectRequiredTools } from './util/mcp.js'; 5 | import { writeReport } from './util/reports.js'; 6 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions'; // Keep OpenAI type for structure 7 | import { writeObservation, getAgentObservations } from './util/observations.js'; 8 | import { callLlm, getScenarioAgentPrompt } from './util/agent-utils.js'; 9 | 10 | const MAX_RUNTIME = 15 * 60 * 1000; // 15 minutes 11 | 12 | // Define LlmConfig interface (can be moved to a shared types file later if needed) 13 | interface LlmConfig { 14 | provider?: string; 15 | model?: string; 16 | maxTokens?: number; 17 | apiKey?: string; // Generic key, primarily used for OpenRouter for backward compatibility 18 | openrouterApiKey?: string; // Alias for apiKey, prefer this for new code 19 | baseURL?: string; // For OpenAI-compatible APIs 20 | openaiApiKey?: string; // For OpenAI and compatible providers 21 | geminiApiKey?: string; 22 | anthropicApiKey?: string; 23 | } 24 | 25 | interface ScenarioArgs { 26 | id: string; 27 | session: string; 28 | error: string; 29 | context: string; 30 | hypothesis: string; 31 | language: string; 32 | repoPath: string; 33 | filePath?: string; 34 | branch: string; 35 | } 36 | 37 | function parseArgs(args: string[]): ScenarioArgs { 38 | const result: Record = {}; 39 | for (let i = 0; i < args.length; i++) { 40 | if (args[i].startsWith('--')) { 41 | const key = args[i].slice(2); 42 | const value = args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : ''; 43 | result[key] = value; 44 | if (value) i++; 45 | } 46 | } 47 | 48 | const repoPath = result.repo; 49 | if (!repoPath) { 50 | throw new Error('Required argument missing: --repo'); 51 | } 52 | 53 | return { 54 | id: result.id || '', 55 | session: result.session || '', 56 | error: result.error || '', 57 | context: result.context || '', 58 | hypothesis: result.hypothesis || '', 59 | language: result.language || 'typescript', 60 | repoPath, 61 | filePath: result.file || undefined, 62 | branch: result.branch || '' 63 | }; 64 | } 65 | 66 | export async function runScenarioAgent(args: ScenarioArgs) { 67 | await log(args.session, `scenario-${args.id}`, 'info', 'Scenario agent started', { repoPath: args.repoPath, hypothesis: args.hypothesis }); 68 | await log( 69 | args.session, 70 | `scenario-${args.id}`, 71 | 'debug', 72 | `CWD: ${process.cwd()}, DEEBO_NPX_PATH=${process.env.DEEBO_NPX_PATH}, DEEBO_UVX_PATH=${process.env.DEEBO_UVX_PATH}`, 73 | { repoPath: args.repoPath } 74 | ); 75 | try { 76 | // Set up tools 77 | await log(args.session, `scenario-${args.id}`, 'info', 'Connecting to tools...', { repoPath: args.repoPath }); 78 | const { gitClient, filesystemClient } = await connectRequiredTools( 79 | `scenario-${args.id}`, 80 | args.session, 81 | args.repoPath 82 | ); 83 | await log(args.session, `scenario-${args.id}`, 'info', 'Connected to tools successfully', { repoPath: args.repoPath }); 84 | 85 | // Branch creation is handled by system infrastructure before this agent is spawned. 86 | 87 | // Start LLM conversation with initial context 88 | const startTime = Date.now(); 89 | // Initial conversation context 90 | const messages: ChatCompletionMessageParam[] = [{ 91 | role: 'assistant', 92 | content: getScenarioAgentPrompt({ 93 | branch: args.branch, 94 | hypothesis: args.hypothesis, 95 | context: args.context, 96 | repoPath: args.repoPath 97 | }) 98 | }, { 99 | role: 'user', 100 | content: `Error: ${args.error} 101 | Context: ${args.context} 102 | Language: ${args.language} 103 | File: ${args.filePath} 104 | Repo: ${args.repoPath} 105 | Hypothesis: ${args.hypothesis}` 106 | }]; 107 | 108 | // Check for observations (initial load) 109 | let observations = await getAgentObservations(args.repoPath, args.session, `scenario-${args.id}`); 110 | if (observations.length > 0) { 111 | messages.push(...observations.map((obs: string): ChatCompletionMessageParam => ({ // Added explicit type 112 | role: 'user' as const, 113 | content: `Scientific observation: ${obs}` 114 | }))); 115 | } 116 | 117 | // Read LLM configuration from environment variables 118 | const scenarioProvider = process.env.SCENARIO_HOST; // Read provider name from SCENARIO_HOST 119 | const scenarioModel = process.env.SCENARIO_MODEL; 120 | const openrouterApiKey = process.env.OPENROUTER_API_KEY; // Still needed if provider is 'openrouter' 121 | const openaiApiKey = process.env.OPENAI_API_KEY; 122 | const openaiBaseUrl = process.env.OPENAI_BASE_URL; 123 | const geminiApiKey = process.env.GEMINI_API_KEY; 124 | const anthropicApiKey = process.env.ANTHROPIC_API_KEY; 125 | 126 | // Create the config object to pass to callLlm 127 | const llmConfig: LlmConfig = { 128 | provider: scenarioProvider, // Use the provider name from SCENARIO_HOST 129 | model: scenarioModel, 130 | apiKey: openrouterApiKey, 131 | openrouterApiKey: openrouterApiKey, // For OpenRouter 132 | openaiApiKey: openaiApiKey, // For OpenAI and compatible providers 133 | baseURL: openaiBaseUrl, // For OpenAI-compatible APIs 134 | geminiApiKey: geminiApiKey, 135 | anthropicApiKey: anthropicApiKey 136 | }; 137 | 138 | await log(args.session, `scenario-${args.id}`, 'debug', 'Sending to LLM', { model: llmConfig.model, provider: llmConfig.provider, messages, repoPath: args.repoPath }); 139 | 140 | // Add retry logic with exponential backoff for initial call 141 | let consecutiveFailures = 0; 142 | const MAX_RETRIES = 3; 143 | let replyText: string | undefined; 144 | 145 | while (consecutiveFailures < MAX_RETRIES) { 146 | replyText = await callLlm(messages, llmConfig); 147 | 148 | if (!replyText) { 149 | // Log the failure and increment counter 150 | consecutiveFailures++; 151 | await log(args.session, `scenario-${args.id}`, 'warn', `Received empty/malformed response from LLM on initial call (Failure ${consecutiveFailures}/${MAX_RETRIES})`, { provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 152 | 153 | // Push a message indicating the failure to help LLM recover 154 | messages.push({ 155 | role: 'user', 156 | content: `INTERNAL_NOTE: Initial LLM call failed to return valid content (Attempt ${consecutiveFailures}/${MAX_RETRIES}). Please try again.` 157 | }); 158 | 159 | // Add exponential backoff delay 160 | const delay = 2000 * Math.pow(2, consecutiveFailures - 1); 161 | await new Promise(resolve => setTimeout(resolve, delay)); 162 | 163 | // Try again if we haven't hit max retries 164 | if (consecutiveFailures < MAX_RETRIES) { 165 | continue; 166 | } 167 | 168 | // Max retries hit - write report and exit 169 | const errorMsg = `Initial LLM call failed to return valid response after ${MAX_RETRIES} attempts`; 170 | await log(args.session, `scenario-${args.id}`, 'error', errorMsg, { provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 171 | await writeReport(args.repoPath, args.session, args.id, errorMsg); 172 | console.log(errorMsg); 173 | process.exit(1); 174 | } 175 | 176 | // Valid response received 177 | messages.push({ role: 'assistant', content: replyText }); 178 | await log(args.session, `scenario-${args.id}`, 'debug', 'Received response from LLM', { response: { content: replyText }, repoPath: args.repoPath }); 179 | break; // Exit retry loop on success 180 | } 181 | 182 | // --- Main Investigation Loop --- 183 | while (true) { 184 | if (Date.now() - startTime > MAX_RUNTIME) { 185 | const timeoutMsg = 'Investigation exceeded maximum runtime'; 186 | await log(args.session, `scenario-${args.id}`, 'warn', timeoutMsg, { repoPath: args.repoPath }); 187 | await writeReport(args.repoPath, args.session, args.id, timeoutMsg); 188 | console.log(timeoutMsg); 189 | process.exit(1); 190 | } 191 | 192 | // Get the latest assistant response 193 | if (!replyText) { 194 | const errorMsg = 'Unexpected undefined response in main loop'; 195 | await log(args.session, `scenario-${args.id}`, 'error', errorMsg, { repoPath: args.repoPath }); 196 | await writeReport(args.repoPath, args.session, args.id, errorMsg); 197 | console.log(errorMsg); 198 | process.exit(1); 199 | } 200 | 201 | // --- Check for Report and Tool Calls --- 202 | const toolCalls = replyText.match(/[\s\S]*?<\/use_mcp_tool>/g) || []; 203 | const reportMatch = replyText.match(/\s*([\s\S]*?)<\/report>/i); 204 | 205 | let executeToolsThisTurn = false; 206 | let exitThisTurn = false; 207 | 208 | if (reportMatch && toolCalls.length > 0) { 209 | // LLM included both - prioritize executing tools, ignore report this turn 210 | messages.push({ 211 | role: 'user', 212 | content: `Instructions conflict: You provided tool calls and a report in the same message. I will execute the tool calls now. Provide the report ONLY after analyzing the tool results in the next turn.` 213 | }); 214 | executeToolsThisTurn = true; // Signal to execute tools below 215 | await log(args.session, `scenario-${args.id}`, 'warn', 'LLM provided tools and report simultaneously. Executing tools, ignoring report.', { repoPath: args.repoPath }); 216 | 217 | } else if (reportMatch) { 218 | // Only report found - process it and exit 219 | const reportText = reportMatch[1].trim(); 220 | await log(args.session, `scenario-${args.id}`, 'info', 'Report found. Writing report and exiting.', { repoPath: args.repoPath }); 221 | await writeReport(args.repoPath, args.session, args.id, reportText); 222 | console.log(reportText); // Print report to stdout for mother agent 223 | exitThisTurn = true; // Signal to exit loop cleanly 224 | 225 | } else if (toolCalls.length > 0) { 226 | // Only tool calls found - execute them 227 | executeToolsThisTurn = true; // Signal to execute tools below 228 | await log(args.session, `scenario-${args.id}`, 'debug', `Found ${toolCalls.length} tool calls to execute.`, { repoPath: args.repoPath }); 229 | } 230 | // If neither tools nor report found, the loop continues to the next LLM call 231 | 232 | // Exit now if a report-only response was processed 233 | if (exitThisTurn) { 234 | process.exit(0); 235 | } 236 | 237 | // --- Execute Tools if Flagged --- 238 | if (executeToolsThisTurn) { 239 | const parsedCalls = toolCalls.map((tc: string) => { 240 | try { 241 | const serverNameMatch = tc.match(/(.*?)<\/server_name>/); 242 | if (!serverNameMatch || !serverNameMatch[1]) throw new Error('Missing server_name'); 243 | const serverName = serverNameMatch[1]; 244 | const server = serverName === 'git-mcp' ? gitClient! : filesystemClient!; // Select client based on name 245 | if (!server) throw new Error(`Invalid server_name: ${serverName}`); 246 | 247 | const toolMatch = tc.match(/(.*?)<\/tool_name>/); 248 | if (!toolMatch || !toolMatch[1]) throw new Error('Missing tool_name'); 249 | const tool = toolMatch[1]!; 250 | 251 | const argsMatch = tc.match(/(.*?)<\/arguments>/s); 252 | if (!argsMatch || !argsMatch[1]) throw new Error('Missing arguments'); 253 | const args = JSON.parse(argsMatch[1]!); 254 | 255 | return { server, tool, args }; 256 | } catch (err) { 257 | const errorMsg = err instanceof Error ? err.message : String(err); 258 | log(args.session, `scenario-${args.id}`, 'error', `Failed to parse tool call: ${errorMsg}`, { toolCall: tc, repoPath: args.repoPath }); 259 | return { error: errorMsg }; // Return error object for specific call 260 | } 261 | }); 262 | 263 | // Process each parsed call - add results or errors back to messages 264 | let toolCallFailed = false; 265 | for (const parsed of parsedCalls) { 266 | if ('error' in parsed) { 267 | messages.push({ 268 | role: 'user', 269 | content: `Tool call parsing failed: ${parsed.error}` 270 | }); 271 | toolCallFailed = true; // Mark failure, but continue processing other calls if needed, or let LLM handle it next turn 272 | continue; // Skip execution for this malformed call 273 | } 274 | 275 | // Prevent disallowed tools 276 | if (parsed.tool === 'git_create_branch') { 277 | messages.push({ 278 | role: 'user', 279 | content: 'Error: Tool call `git_create_branch` is not allowed. The branch was already created by the mother agent.' 280 | }); 281 | await log(args.session, `scenario-${args.id}`, 'warn', `Attempted disallowed tool call: ${parsed.tool}`, { repoPath: args.repoPath }); 282 | continue; // Skip this specific call 283 | } 284 | 285 | try { 286 | await log(args.session, `scenario-${args.id}`, 'debug', `Executing tool: ${parsed.tool}`, { args: parsed.args, repoPath: args.repoPath }); 287 | const result = await parsed.server.callTool({ name: parsed.tool, arguments: parsed.args }); 288 | messages.push({ 289 | role: 'user', 290 | content: JSON.stringify(result) // Tool results are added as user messages 291 | }); 292 | await log(args.session, `scenario-${args.id}`, 'debug', `Tool result for ${parsed.tool}`, { result: result, repoPath: args.repoPath }); 293 | } catch (toolErr) { 294 | const errorMsg = toolErr instanceof Error ? toolErr.message : String(toolErr); 295 | messages.push({ 296 | role: 'user', 297 | content: `Tool call failed for '${parsed.tool}': ${errorMsg}` 298 | }); 299 | await log(args.session, `scenario-${args.id}`, 'error', `Tool call execution failed: ${parsed.tool}`, { error: errorMsg, repoPath: args.repoPath }); 300 | toolCallFailed = true; // Mark failure 301 | } 302 | } 303 | // Decide if we should immediately ask LLM again after tool failure, or let the loop naturally continue. 304 | // Current logic lets loop continue, LLM will see the error messages. 305 | } 306 | 307 | // --- Check for New Observations --- 308 | const newObservations = await getAgentObservations(args.repoPath, args.session, `scenario-${args.id}`); 309 | if (newObservations.length > observations.length) { 310 | const latestObservations = newObservations.slice(observations.length); 311 | messages.push(...latestObservations.map((obs: string): ChatCompletionMessageParam => ({ 312 | role: 'user', 313 | content: `Scientific observation: ${obs}` 314 | }))); 315 | observations = newObservations; // Update the baseline observation list 316 | await log(args.session, `scenario-${args.id}`, 'debug', `Added ${latestObservations.length} new observations to context.`, { repoPath: args.repoPath }); 317 | } 318 | 319 | // --- Make Next LLM Call --- 320 | await log(args.session, `scenario-${args.id}`, 'debug', `Sending message history (${messages.length} items) to LLM`, { model: llmConfig.model, provider: llmConfig.provider, repoPath: args.repoPath }); 321 | 322 | // Add retry logic with exponential backoff 323 | let consecutiveFailures = 0; 324 | const MAX_RETRIES = 3; 325 | 326 | while (consecutiveFailures < MAX_RETRIES) { 327 | replyText = await callLlm(messages, llmConfig); 328 | 329 | if (!replyText) { 330 | // Log the failure and increment counter 331 | consecutiveFailures++; 332 | await log(args.session, `scenario-${args.id}`, 'warn', `Received empty/malformed response from LLM (Failure ${consecutiveFailures}/${MAX_RETRIES})`, { provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 333 | 334 | // Push a message indicating the failure to help LLM recover 335 | messages.push({ 336 | role: 'user', 337 | content: `INTERNAL_NOTE: Previous LLM call failed to return valid content (Attempt ${consecutiveFailures}/${MAX_RETRIES}). Please try again.` 338 | }); 339 | 340 | // Add exponential backoff delay 341 | const delay = 2000 * Math.pow(2, consecutiveFailures - 1); 342 | await new Promise(resolve => setTimeout(resolve, delay)); 343 | 344 | // Try again if we haven't hit max retries 345 | if (consecutiveFailures < MAX_RETRIES) { 346 | continue; 347 | } 348 | 349 | // Max retries hit - write report and exit 350 | const errorMsg = `LLM failed to return valid response after ${MAX_RETRIES} attempts`; 351 | await log(args.session, `scenario-${args.id}`, 'error', errorMsg, { provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 352 | await writeReport(args.repoPath, args.session, args.id, errorMsg); 353 | console.log(errorMsg); 354 | process.exit(1); 355 | } 356 | 357 | // Valid response received 358 | messages.push({ role: 'assistant', content: replyText }); 359 | await log(args.session, `scenario-${args.id}`, 'debug', 'Received response from LLM', { responseLength: replyText.length, provider: llmConfig.provider, model: llmConfig.model, repoPath: args.repoPath }); 360 | break; // Exit retry loop on success 361 | } 362 | 363 | // Small delay before next iteration (optional) 364 | await new Promise(resolve => setTimeout(resolve, 1000)); 365 | } 366 | } catch (error) { 367 | // Catch unexpected errors during setup or within the loop if not handled 368 | const errorText = error instanceof Error ? `${error.message}${error.stack ? `\nStack: ${error.stack}` : ''}` : String(error); 369 | await log(args.session, `scenario-${args.id}`, 'error', `Unhandled scenario error: ${errorText}`, { repoPath: args.repoPath }); 370 | await writeReport(args.repoPath, args.session, args.id, `SCENARIO FAILED UNEXPECTEDLY: ${errorText}`); 371 | console.error(`SCENARIO FAILED UNEXPECTEDLY: ${errorText}`); // Log error to stderr as well 372 | process.exit(1); 373 | } 374 | } 375 | 376 | // --- Script Entry Point --- 377 | try { 378 | const args = parseArgs(process.argv.slice(2)); // Pass relevant args, skipping node path and script path 379 | runScenarioAgent(args); // No await here, let the async function run 380 | } catch (err) { 381 | // Handle argument parsing errors 382 | const errorText = err instanceof Error ? err.message : String(err); 383 | console.error(`Scenario agent failed to start due to arg parsing error: ${errorText}`); 384 | // Attempt to log if possible, though session info might be missing 385 | // log(args.session || 'unknown', `scenario-${args.id || 'unknown'}`, 'error', `Arg parsing failed: ${errorText}`, {}).catch(); 386 | process.exit(1); 387 | } 388 | 389 | // Optional: Add unhandled rejection/exception handlers for more robustness 390 | process.on('unhandledRejection', (reason, promise) => { 391 | console.error('Unhandled Rejection at:', promise, 'reason:', reason); 392 | // Log this? Might be hard without session context. 393 | process.exit(1); // Exit on unhandled promise rejection 394 | }); 395 | 396 | process.on('uncaughtException', (error) => { 397 | console.error('Uncaught Exception:', error); 398 | // Log this? 399 | process.exit(1); // Exit on uncaught exception 400 | }); 401 | -------------------------------------------------------------------------------- /src/util/branch-manager.ts: -------------------------------------------------------------------------------- 1 | // src/util/branch-manager.ts 2 | import { simpleGit } from 'simple-git'; 3 | 4 | // note: second parameter is `scenarioId` 5 | export async function createScenarioBranch(repoPath: string, scenarioId: string): Promise { 6 | const git = simpleGit(repoPath); 7 | const branchName = `debug-${scenarioId}`; // e.g. debug-session-1745287764331-0 8 | await git.checkoutLocalBranch(branchName); 9 | return branchName; 10 | } -------------------------------------------------------------------------------- /src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, mkdir } from 'fs/promises'; 2 | import { join } from 'path'; 3 | import { DEEBO_ROOT } from '../index.js'; 4 | import { getProjectId } from './sanitize.js'; 5 | 6 | // Write logs to memory bank structure 7 | export async function log(sessionId: string, name: string, level: string, message: string, data?: any) { 8 | const entry = JSON.stringify({ 9 | timestamp: new Date().toISOString(), 10 | agent: name, 11 | level, 12 | message, 13 | data 14 | }) + '\n'; 15 | 16 | // Data will be written to memory-bank/projectId/sessions/sessionId/logs/agentName.log 17 | const projectId = getProjectId(data?.repoPath); 18 | if (projectId) { 19 | const logPath = join(DEEBO_ROOT, 'memory-bank', projectId, 'sessions', sessionId, 'logs', `${name}.log`); 20 | await writeFile(logPath, entry, { flag: 'a' }); 21 | } 22 | } 23 | 24 | // Simple console logging 25 | export function consoleLog(level: string, message: string, data?: any) { 26 | console.log(`[${level}] ${message}`, data || ''); 27 | } 28 | -------------------------------------------------------------------------------- /src/util/mcp.ts: -------------------------------------------------------------------------------- 1 | // src/util/mcp.ts 2 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 3 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 4 | import { readFile, writeFile } from 'fs/promises'; 5 | import { join } from 'path'; 6 | import * as path from 'path'; 7 | import { DEEBO_ROOT } from '../index.js'; 8 | import { getProjectId } from './sanitize.js'; 9 | 10 | // Map to track active connections 11 | const activeConnections: Map> = new Map(); 12 | 13 | export async function connectMcpTool(name: string, toolName: string, sessionId: string, repoPath: string) { 14 | const rawConfig = JSON.parse(await readFile(join(DEEBO_ROOT, 'config', 'tools.json'), 'utf-8')); 15 | const def = rawConfig.tools[toolName]; 16 | const memoryPath = join(DEEBO_ROOT, 'memory-bank', getProjectId(repoPath)); 17 | const memoryRoot = join(DEEBO_ROOT, 'memory-bank'); 18 | 19 | /* --- WINDOWS-ONLY PATCH ----------------------------------------- */ 20 | if (process.platform === "win32" && toolName === "desktopCommander") { 21 | // Use the real *.cmd so the process owns stdin/stdout 22 | const cmdPath = path.join(process.env.DEEBO_NPM_BIN!, "desktop-commander.cmd"); 23 | def.command = cmdPath; 24 | def.args = ["serve"]; // same behaviour as 'npx … serve' 25 | } 26 | /* ---------------------------------------------------------------- */ 27 | 28 | // Substitute npx/uvx paths directly in the command 29 | let command = def.command 30 | .replace(/{npxPath}/g, process.env.DEEBO_NPX_PATH!) 31 | .replace(/{uvxPath}/g, process.env.DEEBO_UVX_PATH!); 32 | 33 | // Replace placeholders in all args 34 | let args = def.args.map((arg: string) => 35 | arg 36 | .replace(/{repoPath}/g, repoPath) 37 | .replace(/{memoryPath}/g, memoryPath) 38 | .replace(/{memoryRoot}/g, memoryRoot) 39 | ); 40 | 41 | // Handle environment variable substitutions 42 | if (def.env) { 43 | for (const [key, value] of Object.entries(def.env)) { 44 | if (typeof value === 'string') { 45 | def.env[key] = value 46 | .replace(/{ripgrepPath}/g, process.env.RIPGREP_PATH!) 47 | .replace(/{repoPath}/g, repoPath) 48 | .replace(/{memoryPath}/g, memoryPath) 49 | .replace(/{memoryRoot}/g, memoryRoot); 50 | } 51 | } 52 | } 53 | 54 | // No shell: spawn the .cmd/binary directly on all platforms 55 | const options = {}; 56 | 57 | const transport = new StdioClientTransport({ 58 | command, 59 | args, 60 | ...options, 61 | env: { 62 | ...process.env, // Inherit all environment variables 63 | // Explicitly set critical variables 64 | NODE_ENV: process.env.NODE_ENV!, 65 | USE_MEMORY_BANK: process.env.USE_MEMORY_BANK!, 66 | MOTHER_HOST: process.env.MOTHER_HOST!, 67 | MOTHER_MODEL: process.env.MOTHER_MODEL!, 68 | SCENARIO_HOST: process.env.SCENARIO_HOST!, 69 | SCENARIO_MODEL: process.env.SCENARIO_MODEL!, 70 | OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY! 71 | } 72 | }); 73 | const client = new Client({ name, version: '1.0.0' }, { capabilities: { tools: true } }); 74 | await client.connect(transport); 75 | return client; 76 | } 77 | 78 | export async function connectRequiredTools(agentName: string, sessionId: string, repoPath: string): Promise<{ 79 | gitClient: Client; 80 | filesystemClient: Client; 81 | }> { 82 | const [gitClient, filesystemClient] = await Promise.all([ 83 | connectMcpTool(`${agentName}-git`, 'git-mcp', sessionId, repoPath), 84 | // Switch from "filesystem-mcp" to "desktop-commander" 85 | connectMcpTool(`${agentName}-desktop-commander`, 'desktopCommander', sessionId, repoPath) 86 | ]); 87 | 88 | return { gitClient, filesystemClient }; 89 | } 90 | -------------------------------------------------------------------------------- /src/util/membank.ts: -------------------------------------------------------------------------------- 1 | // src/util/membank.js 2 | import { join } from 'path'; 3 | import { writeFile } from 'fs/promises'; 4 | import { DEEBO_ROOT } from '../index.js'; 5 | 6 | export async function updateMemoryBank(projectId: string, content: string, file: 'activeContext' | 'progress'): Promise { 7 | const path = join(DEEBO_ROOT, 'memory-bank', projectId, `${file}.md`); 8 | await writeFile(path, '\n' + content, { flag: 'a' }); 9 | } -------------------------------------------------------------------------------- /src/util/observations.ts: -------------------------------------------------------------------------------- 1 | import { writeFile, mkdir, readFile } from 'fs/promises'; 2 | import { join } from 'path'; 3 | import { DEEBO_ROOT } from '../index.js'; 4 | import { getProjectId } from './sanitize.js'; 5 | 6 | export async function getAgentObservations(repoPath: string, sessionId: string, agentId: string): Promise { 7 | const projectId = getProjectId(repoPath); 8 | const obsPath = join(DEEBO_ROOT, 'memory-bank', projectId, 'sessions', sessionId, 'observations', `${agentId}.log`); 9 | 10 | try { 11 | const content = await readFile(obsPath, 'utf8'); 12 | return content 13 | .split('\n') 14 | .filter(Boolean) 15 | .map((line: string) => JSON.parse(line).observation); 16 | } catch { 17 | return []; // No observations yet 18 | } 19 | } 20 | 21 | export async function writeObservation(repoPath: string, sessionId: string, agentId: string, observation: string) { 22 | const projectId = getProjectId(repoPath); 23 | const obsDir = join(DEEBO_ROOT, 'memory-bank', projectId, 'sessions', sessionId, 'observations'); 24 | await mkdir(obsDir, { recursive: true }); 25 | 26 | const entry = JSON.stringify({ 27 | timestamp: new Date().toISOString(), 28 | observation 29 | }) + '\n'; 30 | 31 | await writeFile(join(obsDir, `${agentId}.log`), entry, { flag: 'a' }); 32 | } 33 | -------------------------------------------------------------------------------- /src/util/reports.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from "fs/promises"; 2 | import { join } from "path"; 3 | import { DEEBO_ROOT } from "../index.js"; 4 | import { getProjectId } from "./sanitize.js"; 5 | 6 | export async function writeReport( 7 | repoPath: string, 8 | sessionId: string, 9 | scenarioId: string, 10 | report: any 11 | ) { 12 | const projectId = getProjectId(repoPath); 13 | const reportDir = join( 14 | DEEBO_ROOT, 15 | "memory-bank", 16 | projectId, 17 | "sessions", 18 | sessionId, 19 | "reports" 20 | ); 21 | await mkdir(reportDir, { recursive: true }); 22 | 23 | // pretty-print with 2-space indent 24 | const reportPath = join(reportDir, `${scenarioId}.json`); 25 | await writeFile(reportPath, JSON.stringify(report, null, 2), "utf8"); 26 | } -------------------------------------------------------------------------------- /src/util/sanitize.ts: -------------------------------------------------------------------------------- 1 | // src/util/sanitize.ts 2 | import { createHash } from 'crypto'; 3 | 4 | export function getProjectId(repoPath: string): string { 5 | const hash = createHash('sha256').update(repoPath).digest('hex'); 6 | return hash.slice(0, 12); // use first 12 characters 7 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "allowJs": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "lib": ["ES2022"], 16 | "types": ["node"] 17 | }, 18 | "include": ["src/**/*"] 19 | } 20 | --------------------------------------------------------------------------------