├── .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 | [](https://github.com/snagasuri/deebo-prototype/actions/workflows/basic-ci.yml)
3 | [](https://www.npmjs.com/package/deebo-setup)
4 | [](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 |
--------------------------------------------------------------------------------