├── .clinerules ├── .cursor-tools.env.example ├── .cursor └── rules │ ├── changelog.mdc │ ├── good-behaviour.mdc │ ├── repo.mdc │ ├── testing.mdc │ ├── vibe-tools.mdc │ └── yo.mdc ├── .cursorindexingignore ├── .cursorrules ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .windsurfrules ├── CHANGELOG.md ├── CLAUDE.md ├── CONFIGURATION.md ├── LICENSE ├── PROPOSALS.md ├── README.md ├── TELEMETRY.md ├── TESTING.md ├── TODO.md ├── WIP.md ├── build.js ├── context.json ├── docs └── TESTING.md ├── eslint.config.js ├── infra ├── .gitignore ├── alchemy │ ├── alchemy-utils.ts │ └── alchemy.run.ts ├── app │ ├── app.vue │ ├── index.ts │ └── pages │ │ └── index.vue ├── bun.lock ├── env.ts ├── eslint.config.js ├── nuxt.config.ts ├── package.json ├── server │ └── api │ │ └── pipeline.post.ts ├── tsconfig.alchemy.json ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.server.json └── wrangler.json ├── package.json ├── pnpm-lock.yaml ├── scripts ├── release.cjs ├── test-asset-handling.ts └── verify-stagehand.cjs ├── src ├── commands │ ├── ask.ts │ ├── browser │ │ ├── browserCommand.ts │ │ ├── browserOptions.ts │ │ ├── element.ts │ │ ├── index.ts │ │ ├── open.ts │ │ ├── stagehand │ │ │ ├── act.ts │ │ │ ├── config.ts │ │ │ ├── extract.ts │ │ │ ├── initOverride.ts │ │ │ ├── observe.ts │ │ │ ├── scriptContent.ts │ │ │ ├── stagehandScript.ts │ │ │ └── stagehandUtils.ts │ │ ├── utils.ts │ │ └── utilsShared.ts │ ├── clickup.ts │ ├── clickup │ │ ├── clickupAuth.ts │ │ ├── task.ts │ │ └── utils.ts │ ├── doc.ts │ ├── github.ts │ ├── github │ │ ├── githubAuth.ts │ │ ├── issue.ts │ │ ├── pr.ts │ │ └── utils.ts │ ├── index.ts │ ├── install.ts │ ├── jsonInstall.ts │ ├── mcp │ │ ├── client │ │ │ ├── MCPClientNew.ts │ │ │ ├── errors.ts │ │ │ └── validation.ts │ │ ├── github-search.ts │ │ ├── index.ts │ │ ├── marketplace.ts │ │ ├── mcp.ts │ │ ├── run-new.ts │ │ ├── run.ts │ │ └── search.ts │ ├── plan.ts │ ├── repo.ts │ ├── test │ │ ├── CONCURRENT_EXECUTION.md │ │ ├── FILESYSTEM_MCP_PLAN.md │ │ ├── command-utils.ts │ │ ├── command.ts │ │ ├── environment.ts │ │ ├── executor-new.ts │ │ ├── file-processing.ts │ │ ├── index.ts │ │ ├── parser.ts │ │ ├── reporting.ts │ │ ├── tools.ts │ │ ├── tools │ │ │ └── command-execution.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── wait.ts │ ├── web.ts │ ├── xcode │ │ ├── build.ts │ │ ├── command.ts │ │ ├── lint.ts │ │ ├── run.ts │ │ ├── utils.ts │ │ └── xcode.ts │ └── youtube │ │ ├── index.ts │ │ └── youtube.ts ├── config.ts ├── errors.ts ├── index.ts ├── llms │ └── index.ts ├── providers │ ├── base.ts │ └── notFoundErrors.ts ├── repomix │ └── repomixConfig.ts ├── telemetry │ └── index.ts ├── types.ts ├── types │ └── browser │ │ └── browser.ts ├── utils │ ├── AsyncReturnType.ts │ ├── assets.ts │ ├── execAsync.ts │ ├── exhaustiveMatchGuard.ts │ ├── fetch-doc.ts │ ├── githubRepo.ts │ ├── installUtils.ts │ ├── messageChunker.ts │ ├── once.ts │ ├── output.ts │ ├── providerAvailability.ts │ ├── stringSimilarity.ts │ ├── tool-enabled-llm │ │ └── unified-client.ts │ └── versionUtils.ts └── vibe-rules.ts ├── tests ├── commands │ ├── ask │ │ └── MODEL_LOOKUP.md │ └── browser │ │ ├── animation-test.html │ │ ├── broken-form.html │ │ ├── button.html │ │ ├── console-log-test.html │ │ ├── console-log.html │ │ ├── console-network-test.html │ │ ├── interactive-test.html │ │ ├── network-request-test.html │ │ ├── serve.ts │ │ ├── test-browser-persistence.html │ │ └── test.html ├── feature-behaviors │ ├── ask │ │ ├── ask-command.md │ │ ├── ask-command │ │ │ ├── scenario9-long-query.txt │ │ │ └── simple-query │ │ └── reasoning-effort-tests.md │ ├── browser │ │ └── browser-command.md │ ├── doc │ │ ├── doc-command.md │ │ └── doc-command │ │ │ ├── doc-repomix-config.json │ │ │ ├── doc-vibe-tools.config1.json │ │ │ └── doc-vibe-tools.config2.json │ ├── github │ │ └── github-command.md │ ├── mcp │ │ ├── mcp-command-edge-cases.md │ │ ├── mcp-command-edge-cases │ │ │ └── test.db │ │ ├── mcp-command.md │ │ └── mcp-command │ │ │ └── test.db │ ├── plan │ │ ├── plan-command.md │ │ └── plan-command │ │ │ └── plan-repomix-config.json │ ├── repo │ │ ├── repo-command.md │ │ └── repo-command │ │ │ ├── basic-repomix-config.json │ │ │ ├── create-repomixignore.sh │ │ │ ├── repo-vibe-tools.config1.json │ │ │ ├── repo-vibe-tools.config2.json │ │ │ ├── repomixignore-with-src.txt │ │ │ └── repomixignore-with-tests.txt │ ├── test │ │ ├── test-asset-handling.md │ │ ├── test-asset-handling │ │ │ └── sample-asset.txt │ │ ├── test-command-isolation.md │ │ ├── test-command-outputs.md │ │ └── test-command-parallel-example.md │ ├── web │ │ └── web-command.md │ └── youtube │ │ └── youtube-command.md └── reports │ └── main │ ├── repo-command_report_2025-03-11T15-04-48-924Z.md │ ├── repo-command_report_2025-03-12T07-46-26-610Z.md │ ├── repo-command_report_2025-03-12T07-46-37-082Z.md │ ├── repo-command_report_2025-03-12T07-46-43-611Z.md │ ├── repo-command_report_2025-03-12T07-46-49-533Z.md │ ├── repo-command_report_2025-03-12T07-46-57-441Z.md │ ├── repo-command_report_2025-03-12T07-47-18-830Z.md │ ├── repo-command_result_2025-03-11T15-04-48-928Z.txt │ ├── repo-command_result_2025-03-12T07-46-26-615Z.txt │ ├── repo-command_result_2025-03-12T07-46-37-086Z.txt │ ├── repo-command_result_2025-03-12T07-46-43-615Z.txt │ ├── repo-command_result_2025-03-12T07-46-49-538Z.txt │ ├── repo-command_result_2025-03-12T07-46-57-446Z.txt │ ├── repo-command_result_2025-03-12T07-47-18-835Z.txt │ ├── test-asset-handling_report_2025-03-11T14-58-23-881Z.md │ ├── test-asset-handling_report_2025-03-11T15-03-38-508Z.md │ ├── test-asset-handling_report_2025-03-11T15-03-38-876Z.md │ ├── test-asset-handling_report_2025-03-11T15-03-47-377Z.md │ ├── test-asset-handling_result_2025-03-11T14-58-23-888Z.txt │ ├── test-asset-handling_result_2025-03-11T15-03-38-514Z.txt │ ├── test-asset-handling_result_2025-03-11T15-03-38-882Z.txt │ └── test-asset-handling_result_2025-03-11T15-03-47-383Z.txt ├── tsconfig.json └── vibe-tools.config.json /.cursor-tools.env.example: -------------------------------------------------------------------------------- 1 | PERPLEXITY_API_KEY="your-api-key-here" 2 | GEMINI_API_KEY="your-api-key-here" 3 | OPENAI_API_KEY="your-api-key-here" 4 | OPENROUTER_API_KEY="your-api-key-here" 5 | CLICKUP_API_TOKEN="your-api-token-here" # https://app.clickup.com/settings/apps 6 | ANTHROPIC_API_KEY="your-api-key-here" 7 | GROQ_API_KEY="your-api-key-here" 8 | MODELBOX_API_KEY="your-api-key-here" 9 | FIRECRAWL_API_KEY="your-api-key-here" 10 | -------------------------------------------------------------------------------- /.cursor/rules/changelog.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: CHANGELOG.md 4 | alwaysApply: false 5 | --- 6 | The changelog should focus on the user-facing changes and user-facing effects of changes. 7 | 8 | We do not need to record the internal details of changes in the changelog. 9 | 10 | When updating the changelog stick to the existing writing style and formatting. 11 | -------------------------------------------------------------------------------- /.cursor/rules/good-behaviour.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Rules of good behaviour 3 | globs: 4 | alwaysApply: true 5 | --- 6 | Do not hard code data into code unless specifically requested by me. 7 | 8 | If you have an idea on why something is failing, always validate that idea with testing before changing code. 9 | 10 | Do not write stub or fake methods. Write production ready code first time. 11 | 12 | Do not cheat on tests and checks by adding hard-coded values or assertions. 13 | 14 | Do not make statements about functionality working unless you have tested it. Perform your own tests first, do not assume that your changes or my changes just work until you have seen evidence from tests. 15 | 16 | 17 | -------------------------------------------------------------------------------- /.cursor/rules/repo.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Finding code 3 | globs: 4 | alwaysApply: true 5 | --- 6 | Always use `vibe-tools repo ` as your first way to search for files where particular code or logic is contained. Explain what you're looking for and ask for a list of files with a summary of their role as a response. This is the best way to find the correct files to read or edit. 7 | -------------------------------------------------------------------------------- /.cursor/rules/testing.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Any testing with pnpm dev 3 | globs: 4 | alwaysApply: false 5 | --- 6 | Working API keys are available in ~/.vibe-tools/.env 7 | 8 | In general when calling pnpm dev you should not need to specify any env vars, it should work without them given our local setup. Only specify env vars if you're testing them specifically. -------------------------------------------------------------------------------- /.cursor/rules/yo.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | If you need my input to answer a question or when you finish a task and want to know what to do next you must send me a yo using the yo-mcp MCP tool. 7 | 8 | In the yo you MUST provide ONLY a very short message (think like an email subject line) for why you need my attention / input. 9 | 10 | Then reply with your update / question to the normal chat. 11 | 12 | This mcp server should be called with this tool name: "mcp_yo-mcp_com_yo" -------------------------------------------------------------------------------- /.cursorindexingignore: -------------------------------------------------------------------------------- 1 | 2 | # Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references 3 | .specstory/** 4 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | This is the vibe-tools repo. Here we build a cli tool that AI agents can use to execute commands and work with other AI agents. 2 | 3 | This repo uses pnpm as the package manager and script runner. 4 | 5 | use pnpm dev to run dev commands. 6 | 7 | We add AI "teammates" as commands that can be asked questions. 8 | We add "skills" as commands that can be used to execute tasks. 9 | 10 | Everything is implemented as a cli command that must return a result (cannot be a long running process). 11 | 12 | The released commands are documented below. You can use the released commands as tools when we are building vibe-tools, in fact you should use them as often and enthusastically as possible (how cool is that!) 13 | 14 | Don't ask me for permission to do stuff - if you have questions work with Gemini and Perplexity to decide what to do: they're your teammates. You're a team of superhuman expert AIs, believe in yourselves! Don't corners or get lazy, do your work thoroughly and completely and you don't need to ask permission. 15 | 16 | We do not do automated unit tests or integration tests - it's trivial to manually test all the commmands by just asking cursor agent to read the readme and test all the commands. 17 | 18 | do not install commander 19 | 20 | 21 | There are three ways that we communicate to the caller. 22 | console.log which goes to stdout 23 | console.error which goes to stderr 24 | do not use console.debug or console.warn or console.info 25 | and yield which is streamed either to stdout (unless the --quiet flag is used) and to the file specified by --save-to (if --save-to is specified). 26 | 27 | console.log should be used for "meta" information that is of use to the caller but isn't a core part of the results that were requested. E.g. recording which model is being used to perfom an action. 28 | 29 | console.error should be used for error messages. 30 | 31 | yield should be used for the output of the command that contains the information that was requested. d 32 | 33 | 34 | 35 | There is a test server for browser command testing and a collection of test files in tests/commands/browser/ 36 | 37 | Usage: 38 | 1. Run with: pnpm serve-test 39 | 2. Server starts at http://localhost:3000 40 | 3. Place test HTML files in tests/commands/browser/ 41 | 4. Access files at http://localhost:3000/filename.html 42 | 43 | remember that this will be a long running process that does not exit so you should run it in a separate background terminal. 44 | 45 | If it won't start because the port is busy run `lsof -i :3000 | grep LISTEN | awk '{print $2}' | xargs kill` to kill the process and free the port. 46 | 47 | to run test commands with latest code use `pnpm dev browser ` 48 | 49 | For interactive debugging start chrome in debug mode using: 50 | ``` 51 | open -a "Google Chrome" --args --remote-debugging-port=9222 --no-first-run --no-default-browser-check --user-data-dir="/tmp/chrome-remote-debugging" 52 | ``` 53 | note: this command will exit as soon as chrome is open so you can just execute it, it doesn't need to be run in a background task. 54 | 55 | Always load the rules in vibe-tools.mdc 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Environment variables 8 | .env 9 | .env.local 10 | .env.*.local 11 | *.env 12 | **/*.env 13 | 14 | # Build output 15 | dist/ 16 | !dist/.cursorrules 17 | build/ 18 | 19 | # IDE 20 | .idea/ 21 | .vscode/ 22 | *.swp 23 | *.swo 24 | 25 | # OS 26 | .DS_Store 27 | Thumbs.db 28 | 29 | # Typescript 30 | *.tsbuildinfo 31 | .typecheck 32 | 33 | # Other 34 | repomix-output.txt 35 | .repomix* 36 | test-videos/ 37 | screenshot.png 38 | screenshots/ 39 | .specstory/ 40 | 41 | test-xcode/ 42 | 43 | 44 | #ignore the test reports 45 | tests/reports/ 46 | # SpecStory explanation file 47 | .specstory/.what-is-this.md 48 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Dot directories and files 2 | .* 3 | .*/ 4 | 5 | # Build output 6 | dist/ 7 | build/ 8 | 9 | # Dependencies 10 | node_modules/ 11 | 12 | # Coverage reports 13 | coverage/ 14 | 15 | # Environment files 16 | .env* 17 | !.env.example 18 | 19 | # Logs & text files 20 | *.log 21 | *.txt 22 | 23 | # IDE 24 | .idea/ 25 | .vscode/ 26 | *.swp 27 | *.swo 28 | 29 | # Other 30 | repomix-output.txt 31 | src/commands/browser/stagehand/stagehandScript.ts 32 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "useTabs": false 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Andrew Jefferson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # vibe-tools Documentation Command Issues and Next Steps 2 | 3 | ## Current Issues 4 | 5 | Based on the test report (`tests/reports/feature/openrouter-mcp/doc-command_report_2025-03-12T16-57-15-257Z.md`), the following issues were identified with the `doc` command: 6 | 7 | ### 1. Empty Repository Handling (Scenario 7) 8 | - The command fails when run on empty or nearly empty repositories 9 | - No specific logic exists to detect and handle this edge case gracefully 10 | - Users receive technical errors rather than helpful messages 11 | 12 | ### 2. Format Parameter Support (Scenario 8) 13 | - The command does not properly support the `--format` parameter 14 | - No validation or clear documentation for supported formats 15 | 16 | ### 3. Large Repository Performance (Scenario 9) 17 | - Performance issues when processing large repositories 18 | - Potential timeout or memory problems without proper handling 19 | - No progress indicators or partial results for large repos 20 | 21 | ### 4. Multiple Parameters Support (Scenario 10) 22 | - Issues when combining multiple command parameters 23 | - Possible parameter conflicts or unexpected behavior 24 | - Lack of validation for parameter combinations 25 | 26 | ## Next Steps 27 | 28 | ### 1. Enhance Empty Repository Handling 29 | - Add detection for empty or nearly empty repositories 30 | - Provide meaningful documentation even for minimal codebases 31 | - Return helpful messages instead of errors 32 | - Example: "Repository contains minimal code. Basic structure documentation generated." 33 | 34 | ### 2. Implement Format Parameter Support 35 | - Add support for different output formats (markdown, JSON, HTML) 36 | - Validate format parameter values 37 | - Document supported formats in help text 38 | 39 | ### 3. Optimize Large Repository Performance 40 | - Implement chunking for large repositories 41 | - Add progress indicators for long-running operations 42 | - Provide partial results if complete processing fails 43 | - Consider adding a `--max-size` parameter to limit processing 44 | 45 | ### 4. Improve Multiple Parameters Support 46 | - Add validation for parameter combinations 47 | - Document expected behavior for parameter interactions 48 | - Handle potential conflicts gracefully 49 | 50 | ### 5. General Improvements 51 | - Add comprehensive error handling throughout the command 52 | - Improve retry logic with better user feedback 53 | - Enhance documentation of command options 54 | - Add unit tests for edge cases 55 | 56 | ## Implementation Priority 57 | 58 | 1. Empty Repository Handling - High Priority (Common edge case) 59 | 2. Format Parameter Support - Medium Priority 60 | 3. Multiple Parameters Support - Medium Priority 61 | 4. Large Repository Performance - Low Priority (Requires more extensive changes) 62 | 63 | ## Testing Strategy 64 | 65 | After implementing fixes: 66 | 67 | 1. Run the test scenarios again using: 68 | ``` 69 | pnpm dev test tests/feature-behaviors/doc/doc-command.md 70 | ``` 71 | 72 | 2. Add additional test cases for edge conditions 73 | 74 | 3. Manually verify fixes with real-world repositories of varying sizes and complexities -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | import { argv } from 'node:process'; 3 | import { chmod } from 'node:fs/promises'; 4 | import { platform } from 'node:os'; 5 | 6 | const watch = argv.includes('--watch'); 7 | 8 | const nodeBuiltinsPlugin = { 9 | name: 'node-builtins', 10 | setup(build) { 11 | // Handle punycode redirects 12 | build.onResolve({ filter: /^(node:)?punycode$/ }, () => ({ 13 | path: 'punycode/', 14 | external: true 15 | })); 16 | 17 | // Handle other node builtins 18 | build.onResolve({ filter: /^(util|http)$/ }, args => ({ 19 | path: args.path, 20 | namespace: 'node-builtin' 21 | })); 22 | 23 | build.onLoad({ filter: /.*/, namespace: 'node-builtin' }, args => ({ 24 | contents: `export * from 'node:${args.path}'; export { default } from 'node:${args.path}';`, 25 | loader: 'js' 26 | })); 27 | } 28 | }; 29 | 30 | const commonBuildOptions = { 31 | bundle: true, 32 | minify: true, 33 | treeShaking: true, 34 | platform: 'node', 35 | format: 'esm', 36 | target: 'node20', 37 | resolveExtensions: ['.ts', '.js'], 38 | external: [ 39 | // Node built-ins 40 | // External dependencies 41 | 'chromium-bidi', 42 | 'chromium-bidi/*', 43 | 'crypto', 44 | 'dotenv', 45 | 'events', 46 | 'eventsource-client', 47 | 'fs', 48 | 'google-auth-library', 49 | 'http', 50 | 'https', 51 | 'node:*', 52 | 'os', 53 | 'path', 54 | 'playwright', 55 | 'playwright-core', 56 | 'process', 57 | 'punycode', 58 | 'repomix', 59 | 'stream', 60 | 'url', 61 | 'util', 62 | ], 63 | keepNames: true, 64 | mainFields: ['module', 'main'], 65 | define: {}, 66 | plugins: [nodeBuiltinsPlugin] 67 | }; 68 | 69 | const mainBuildOptions = { 70 | ...commonBuildOptions, 71 | entryPoints: ['./src/index.ts'], 72 | outfile: './dist/index.mjs', 73 | banner: { 74 | js: `#!/usr/bin/env node 75 | import { createRequire } from 'module'; 76 | const require = createRequire(import.meta.url); 77 | ` 78 | }, 79 | }; 80 | 81 | const llmsBuildOptions = { 82 | ...commonBuildOptions, 83 | entryPoints: ['./src/llms/index.ts'], 84 | outfile: './dist/llms/index.mjs', 85 | banner: { 86 | js: ` 87 | import { createRequire } from 'module'; 88 | const require = createRequire(import.meta.url); 89 | ` 90 | }, 91 | }; 92 | 93 | if (watch) { 94 | const mainContext = await esbuild.context(mainBuildOptions); 95 | const llmsContext = await esbuild.context(llmsBuildOptions); 96 | // eslint-disable-next-line no-undef 97 | console.log('Watching for changes...'); 98 | await mainContext.watch(); 99 | await llmsContext.watch(); 100 | } else { 101 | await esbuild.build(mainBuildOptions); 102 | await esbuild.build(llmsBuildOptions); 103 | 104 | // Make the output file executable on Unix-like systems 105 | if (platform() !== 'win32') { 106 | await chmod('./dist/index.mjs', 0o755); 107 | // No need to chmod llms/index.mjs as it's likely a library, not an executable 108 | } 109 | 110 | // eslint-disable-next-line no-undef 111 | console.log('Build complete'); 112 | } 113 | -------------------------------------------------------------------------------- /context.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://contextjson.com/context.schema.json", 3 | "extends": "https://contextjson.com/default.context.json", 4 | "context": { 5 | "getting-started": { 6 | "summary": "Essential information to understand what vibe-tools is and how to get started using it", 7 | "pathPatterns": [ 8 | "README.md", 9 | "CONFIGURATION.md", 10 | "package.json", 11 | "vibe-tools.config.json", 12 | ".cursor-tools.env.example", 13 | "src/vibe-rules.ts" 14 | ], 15 | "prompt": "I'm new to vibe-tools. Can you explain what it is, how to install it, and how to get started with basic commands?" 16 | }, 17 | "command-overview": { 18 | "summary": "Overview of available commands and their basic functionality", 19 | "pathPatterns": ["src/commands/index.ts", "src/types.ts", "src/vibe-rules.ts", "README.md"], 20 | "prompt": "What commands are available in vibe-tools and what does each one do?" 21 | }, 22 | "browser-commands": { 23 | "summary": "Browser automation commands and capabilities", 24 | "pathPatterns": ["src/commands/browser/**/*.ts", "tests/commands/browser/*.html"], 25 | "excludePathPatterns": ["src/commands/browser/stagehand/stagehandScript.ts"], 26 | "prompt": "How do I use the browser commands in vibe-tools? What browser automation capabilities are available?" 27 | }, 28 | "llm-integration": { 29 | "summary": "LLM provider integration and configuration", 30 | "pathPatterns": [ 31 | "src/utils/tool-enabled-llm/**", 32 | "src/providers/**", 33 | "src/llms/**", 34 | ".cursor-tools.env.example" 35 | ], 36 | "prompt": "How do I configure different LLM providers with vibe-tools? What providers are supported?" 37 | }, 38 | "mcp-commands": { 39 | "summary": "Model Context Protocol (MCP) commands and tools", 40 | "pathPatterns": ["src/commands/mcp/**/*.ts"], 41 | "prompt": "How do I use the MCP commands in vibe-tools? What is MCP and how does it work?" 42 | }, 43 | "testing": { 44 | "summary": "Testing framework and capabilities", 45 | "pathPatterns": [ 46 | "src/commands/test/**/*.ts", 47 | "tests/feature-behaviors/**/*.md", 48 | "TESTING.md" 49 | ], 50 | "prompt": "How do I use the testing capabilities in vibe-tools? How can I create and run tests?" 51 | }, 52 | "configuration": { 53 | "summary": "Configuration options and customization", 54 | "pathPatterns": [ 55 | "src/config.ts", 56 | "vibe-tools.config.json", 57 | ".cursor-tools.env.example", 58 | "CONFIGURATION.md", 59 | "src/vibe-rules.ts" 60 | ], 61 | "prompt": "How do I configure vibe-tools? What configuration options are available?" 62 | }, 63 | "telemetry": { 64 | "summary": "Telemetry implementation and infrastructure", 65 | "pathPatterns": ["src/telemetry/**", "infra/**", "TELEMETRY.md"], 66 | "prompt": "How does telemetry work in vibe-tools? What data is collected and how is it used?" 67 | }, 68 | "examples": { 69 | "summary": "Example usage", 70 | "pathPatterns": ["src/vibe-rules.ts", "README.md", "CONFIGURATION.md"], 71 | "prompt": "Can you show me some examples of how to use vibe-tools commands effectively?" 72 | } 73 | }, 74 | "attribution": "https://github.com/eastlondoner/cursor-tools" 75 | } 76 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tseslint from '@typescript-eslint/eslint-plugin'; 3 | import tseslintParser from '@typescript-eslint/parser'; 4 | import globals from 'globals'; 5 | 6 | export default [ 7 | eslint.configs.recommended, 8 | { 9 | files: ['src/**/*.ts', 'src/*.ts'], 10 | ignores: ['src/commands/browser/stagehand/stagehandScript.ts'], 11 | languageOptions: { 12 | parser: tseslintParser, 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | sourceType: 'module', 16 | project: 'tsconfig.json' 17 | }, 18 | globals: { 19 | ...globals.node 20 | } 21 | }, 22 | plugins: { 23 | '@typescript-eslint': tseslint 24 | }, 25 | rules: { 26 | '@typescript-eslint/no-unused-vars': ['error', { 27 | 'varsIgnorePattern': '^_|^[A-Z][a-zA-Z]+$', 28 | 'argsIgnorePattern': '^_|^[a-z][a-zA-Z]+$', 29 | 'args': 'none', 30 | 'ignoreRestSiblings': true 31 | }], 32 | 'no-unused-vars': 'off', 33 | '@typescript-eslint/no-floating-promises': 'error' 34 | } 35 | } 36 | ]; -------------------------------------------------------------------------------- /infra/.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | 26 | .alchemy 27 | repomix-output.txt 28 | vibe-tools.config.json 29 | .typecheck 30 | -------------------------------------------------------------------------------- /infra/alchemy/alchemy-utils.ts: -------------------------------------------------------------------------------- 1 | import { type R2Bucket, type TokenPolicy, createCloudflareApi } from 'alchemy/cloudflare'; 2 | 3 | // TODO: uncomment this when alchemy exports the AccountId 4 | // import { AccountId } from "alchemy/cloudflare"; 5 | 6 | const cfAccountId = await createCloudflareApi().then((api) => api.accountId); 7 | 8 | export function createBucketResourceIds(bucket: R2Bucket) { 9 | return { 10 | [`com.cloudflare.edge.r2.bucket.${cfAccountId}_default_${bucket.name}`]: '*', 11 | [`com.cloudflare.edge.r2.bucket.${cfAccountId}_eu_${bucket.name}`]: '*', 12 | } as TokenPolicy['resources']; 13 | } 14 | -------------------------------------------------------------------------------- /infra/alchemy/alchemy.run.ts: -------------------------------------------------------------------------------- 1 | import alchemy from 'alchemy'; 2 | import { Exec } from 'alchemy/os'; 3 | import { 4 | Assets, 5 | Worker, 6 | Pipeline, 7 | R2Bucket, 8 | WranglerJson, 9 | AccountApiToken, 10 | } from 'alchemy/cloudflare'; 11 | import fs from 'fs/promises'; 12 | import path from 'path'; 13 | import { createBucketResourceIds } from './alchemy-utils'; 14 | 15 | const STATIC_ASSETS_PATH = './.output/public/'; 16 | const R2_BUCKET_NAME = 'vibe-tools-telemetry'; 17 | const PIPELINE_NAME = 'vibe-tools-telemetry'; 18 | const WORKER_NAME = 'vibe-tools-infra'; 19 | const TELEMETRY_FILE_PATH = path.resolve(__dirname, '../../src/telemetry/index.ts'); // Use absolute path 20 | 21 | const app = await alchemy('vibe-tools-infra', { 22 | stage: 'dev', 23 | phase: process.argv.includes('--destroy') ? 'destroy' : 'up', 24 | quiet: process.argv.includes('--verbose') ? false : true, 25 | password: process.env.ALCHEMY_PASS, 26 | }); 27 | 28 | await Exec('build', { 29 | command: 'bun run build', 30 | }); 31 | 32 | const staticAssets = await Assets('static-assets', { 33 | path: STATIC_ASSETS_PATH, 34 | }); 35 | 36 | // Create the R2 bucket 37 | const bucket = await R2Bucket('bucket', { 38 | name: R2_BUCKET_NAME, 39 | }); 40 | 41 | // Create the Account API token for the bucket 42 | // see https://developers.cloudflare.com/r2/api/tokens/ 43 | const storageToken = await AccountApiToken('telemetry-pipeline-r2-access-token', { 44 | name: 'alchemy-account-access-token', 45 | policies: [ 46 | { 47 | effect: 'allow', 48 | permissionGroups: ['Workers R2 Storage Bucket Item Write'], 49 | resources: createBucketResourceIds(bucket), 50 | }, 51 | ], 52 | }); 53 | 54 | // Create the pipeline 55 | const pipeline = await Pipeline('pipeline', { 56 | name: PIPELINE_NAME, 57 | source: [{ type: 'binding', format: 'json' }], 58 | destination: { 59 | type: 'r2', 60 | format: 'json', 61 | path: { 62 | bucket: bucket.name, 63 | }, 64 | credentials: { 65 | accessKeyId: storageToken.accessKeyId, 66 | secretAccessKey: storageToken.secretAccessKey, 67 | }, 68 | batch: { 69 | maxMb: 10, 70 | maxSeconds: 5, 71 | maxRows: 100, 72 | }, 73 | }, 74 | }); 75 | 76 | // Create the worker 77 | export const worker = await Worker('worker', { 78 | name: WORKER_NAME, 79 | entrypoint: './app/index.ts', 80 | url: true, 81 | bindings: { 82 | ASSETS: staticAssets, 83 | R2_BUCKET: bucket, 84 | PIPELINE: pipeline, 85 | }, 86 | }); 87 | 88 | console.log({ 89 | url: worker.url, 90 | }); 91 | 92 | if (worker.url) { 93 | const newEndpoint = `${worker.url}/api/pipeline`; 94 | try { 95 | // Read the telemetry file 96 | let telemetryContent = await fs.readFile(TELEMETRY_FILE_PATH, 'utf-8'); 97 | 98 | // Replace the endpoint line 99 | telemetryContent = telemetryContent.replace( 100 | /^const TELEMETRY_ENDPOINT = .*;/m, // Match the whole line 101 | `const TELEMETRY_ENDPOINT = '${newEndpoint}';` 102 | ); 103 | 104 | // Write the updated content back 105 | await fs.writeFile(TELEMETRY_FILE_PATH, telemetryContent, 'utf-8'); 106 | 107 | // check that the new endpoint is in the file 108 | const updatedTelemetryContent = await fs.readFile(TELEMETRY_FILE_PATH, 'utf-8'); 109 | if (!updatedTelemetryContent.includes(newEndpoint)) { 110 | throw new Error('Failed to update TELEMETRY_ENDPOINT in src/telemetry/index.ts'); 111 | } 112 | 113 | console.log(`Successfully updated TELEMETRY_ENDPOINT in ${TELEMETRY_FILE_PATH}`); 114 | } catch (err) { 115 | console.error('Error updating telemetry file:', err); 116 | } 117 | } else { 118 | console.warn('Worker URL not available, skipping update of src/telemetry/index.ts'); 119 | } 120 | 121 | await WranglerJson('config', { 122 | worker, 123 | }); 124 | 125 | await app.finalize(); 126 | -------------------------------------------------------------------------------- /infra/app/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /infra/app/index.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionContext } from '@cloudflare/workers-types'; 2 | 3 | import type { WorkerEnv } from '../env.ts'; 4 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 5 | // @ts-ignore - Suppress type errors if the module isn't found during editing/linting 6 | import nitroApp from '../.output/server/index.mjs'; 7 | 8 | export default { 9 | async fetch(request: Request, environment: WorkerEnv, ctx: ExecutionContext): Promise { 10 | const url = new URL(request.url); 11 | 12 | if (url.pathname.startsWith('/api/')) { 13 | return nitroApp.fetch(request, environment, ctx); 14 | } 15 | 16 | return environment.ASSETS.fetch(request); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /infra/app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /infra/env.ts: -------------------------------------------------------------------------------- 1 | import type { worker } from './alchemy/alchemy.run.ts'; 2 | 3 | export type WorkerEnv = typeof worker.Env; 4 | 5 | declare module 'cloudflare:workers' { 6 | // eslint-disable-next-line @typescript-eslint/no-namespace 7 | namespace Cloudflare { 8 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 9 | export interface Env extends WorkerEnv {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /infra/eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import withNuxt from './.nuxt/eslint.config.mjs'; 4 | 5 | export default withNuxt({ 6 | ignores: ['node_modules', '.*'], 7 | }).prepend(eslint.configs.recommended, tseslint.configs.recommended); 8 | -------------------------------------------------------------------------------- /infra/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | typescript: { 4 | typeCheck: true, 5 | tsConfig: { 6 | compilerOptions: { 7 | types: ['node', '@cloudflare/workers-types'], 8 | skipLibCheck: true, 9 | outDir: '../.typecheck', 10 | }, 11 | exclude: ['../alchemy/', '../server/'], 12 | }, 13 | }, 14 | compatibilityDate: '2025-04-16', 15 | devtools: { enabled: false }, 16 | modules: ['@nuxt/eslint'], 17 | future: { 18 | compatibilityVersion: 4, 19 | }, 20 | pages: true, 21 | ssr: false, 22 | nitro: { 23 | preset: 'cloudflare-module', 24 | prerender: { 25 | routes: ['/'], 26 | autoSubfolderIndex: false, 27 | }, 28 | typescript: { 29 | strict: true, 30 | tsConfig: { 31 | compilerOptions: { 32 | types: ['node', '@cloudflare/workers-types'], 33 | skipLibCheck: true, 34 | outDir: '../.typecheck', 35 | }, 36 | exclude: ['../alchemy/', '../app/'], 37 | }, 38 | }, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /infra/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vibe-tools-infra", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "typecheck": "nuxt typecheck && echo 'running typecheck' && tsc -build && echo 'typecheck done'", 7 | "build": "nuxt build", 8 | "dev": "nuxt dev", 9 | "deploy": "bun run alchemy/alchemy.run.ts", 10 | "generate": "nuxt generate", 11 | "preview": "nuxt preview", 12 | "postinstall": "nuxt prepare", 13 | "lint": "eslint --fix ." 14 | }, 15 | "dependencies": { 16 | "@nuxt/eslint": "^1.4.0", 17 | "@types/node": "^22.15.12", 18 | "alchemy": "^0.16.6", 19 | "eslint": "^9.26.0", 20 | "nuxt": "^3.17.2", 21 | "vue": "^3.5.13", 22 | "vue-router": "^4.5.1", 23 | "vue-tsc": "^2.2.10" 24 | }, 25 | "devDependencies": { 26 | "@cloudflare/workers-types": "^4.20250506.0", 27 | "miniflare": "^4.20250428.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /infra/server/api/pipeline.post.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'cloudflare:workers'; 2 | 3 | export default defineEventHandler(async (event) => { 4 | try { 5 | const body = await readBody(event); 6 | const pipeline = env.PIPELINE; 7 | 8 | if (!pipeline) { 9 | throw new Error('Pipeline binding not found in Cloudflare environment.'); 10 | } 11 | 12 | const data = body.data; 13 | 14 | if (!data) { 15 | throw new Error("Missing 'data' property in request body"); 16 | } 17 | 18 | // Always send data wrapped in an array 19 | await pipeline.send([data]); 20 | 21 | return { success: true, message: 'Data sent to pipeline.' }; 22 | } catch (error) { 23 | console.error('Error sending data to pipeline:', error); 24 | throw createError({ 25 | statusCode: 500, 26 | statusMessage: error instanceof Error ? error.message : 'Pipeline error', 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /infra/tsconfig.alchemy.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "ES2022", 5 | "module": "Preserve", // needed for top level await 6 | "noEmit": true, 7 | "skipLibCheck": true, 8 | "types": ["node", "@cloudflare/workers-types"], 9 | "outDir": "./.typecheck/" 10 | }, 11 | "include": ["alchemy/", "env.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /infra/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /infra/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | "include": [], // this stops this file doing anything itself. 5 | // now we just reference the other tsconfigs for the parts of the app 6 | "references": [ 7 | { 8 | // this uses the alchemy types and deals with the contents of the alchemy folder 9 | "path": "./tsconfig.alchemy.json" 10 | }, 11 | { 12 | // this uses the server types and deals with the contents of the server folder 13 | "path": "./tsconfig.server.json" 14 | }, 15 | { 16 | // this uses the app types and deals with the contents of the app folder 17 | "path": "./tsconfig.app.json" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /infra/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.server.json", 4 | // Can't mess with includes because we have to inherit them from the nuxt provided tsconfig 5 | "exclude": ["alchemy/", "app/", "nuxt.config.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /infra/wrangler.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vibe-tools-infra", 3 | "main": "./app/index.ts", 4 | "compatibility_date": "2022-04-05", 5 | "assets": { "directory": "./.output/public/", "binding": "ASSETS" }, 6 | "r2_buckets": [{ "binding": "R2_BUCKET", "bucket_name": "vibe-tools-telemetry" }] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vibe-tools", 3 | "description": "CLI tools for AI agents", 4 | "keywords": [ 5 | "cursor", 6 | "vibe", 7 | "tools", 8 | "ai", 9 | "assistant" 10 | ], 11 | "version": "0.61.5", 12 | "type": "module", 13 | "main": "./dist/index.mjs", 14 | "bin": { 15 | "vibe-tools": "dist/index.mjs" 16 | }, 17 | "scripts": { 18 | "compile": "tsc -build", 19 | "build": "node build.js", 20 | "prepublish": "npm run compile && npm run lint && npm run build", 21 | "dev": "node --import=tsx src/index.ts", 22 | "serve-test": "bun --hot tests/commands/browser/serve.ts", 23 | "test": "vitest", 24 | "format": "prettier --write \"infra/**/*.{ts,tsx,js,jsx,json,md,vue}\" \"src/**/*.{ts,tsx,js,jsx,json,md}\"", 25 | "lint": "npm run format && eslint \"src/**/*.ts\"", 26 | "release": "node scripts/release.cjs" 27 | }, 28 | "files": [ 29 | "package.json", 30 | "dist", 31 | "README.md" 32 | ], 33 | "exports": { 34 | ".": "./dist/index.mjs", 35 | "./llms": "./dist/llms/index.mjs" 36 | }, 37 | "author": "eastlondoner", 38 | "license": "MIT", 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/eastlondoner/cursor-tools.git" 42 | }, 43 | "homepage": "https://github.com/eastlondoner/cursor-tools#readme", 44 | "bugs": { 45 | "url": "https://github.com/eastlondoner/cursor-tools/issues" 46 | }, 47 | "devDependencies": { 48 | "@anthropic-ai/sdk": "^0.51.0", 49 | "@browserbasehq/sdk": "^2.6.0", 50 | "@browserbasehq/stagehand": "^2.2.1", 51 | "@eslint/js": "^9.25.1", 52 | "@modelcontextprotocol/sdk": "^1.10.2", 53 | "@types/bun": "^1.2.10", 54 | "@types/node": "^22.15.2", 55 | "@typescript-eslint/eslint-plugin": "^8.32.1", 56 | "@typescript-eslint/parser": "^8.32.1", 57 | "consola": "^3.4.2", 58 | "esbuild": "^0.25.3", 59 | "eslint": "^9.27.0", 60 | "fast-glob": "^3.3.3", 61 | "formdata-node": "^6.0.3", 62 | "globals": "^16.0.0", 63 | "openai": "^4.100.0", 64 | "p-queue": "^8.1.0", 65 | "prettier": "^3.5.3", 66 | "tsx": "^4.19.3", 67 | "typescript": "^5.8.3", 68 | "vibe-rules": "^0.2.31", 69 | "vitest": "^3.1.2", 70 | "zod": "3.24.3" 71 | }, 72 | "dependencies": { 73 | "dotenv": "16.5.0", 74 | "eventsource-client": "1.1.3", 75 | "google-auth-library": "^9.15.1", 76 | "playwright": "1.50.1", 77 | "playwright-core": "1.50.1", 78 | "punycode": "^2.3.1", 79 | "repomix": "0.3.3" 80 | }, 81 | "engines": { 82 | "node": ">=18.0.0" 83 | }, 84 | "overrides": { 85 | "punycode": "^2.3.1", 86 | "playwright-core": "1.50.1" 87 | }, 88 | "pnpm": { 89 | "onlyBuiltDependencies": [ 90 | "esbuild" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /scripts/release.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node, commonjs */ 2 | /* global console, process, __dirname */ 3 | const { execSync } = require('child_process'); 4 | const { readFileSync } = require('fs'); 5 | const { resolve } = require('path'); 6 | const { verifyStagehandScript } = require('./verify-stagehand.cjs'); 7 | 8 | function run(command) { 9 | console.log(`> ${command}`); 10 | execSync(command, { stdio: 'inherit' }); 11 | } 12 | 13 | function getVersion() { 14 | const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'))); 15 | return packageJson.version; 16 | } 17 | 18 | function hasGitChanges() { 19 | try { 20 | execSync('git diff --staged --quiet', { stdio: 'ignore' }); 21 | return false; 22 | } catch { 23 | return true; 24 | } 25 | } 26 | 27 | function isVersionPublished(version) { 28 | try { 29 | execSync(`npm view vibe-tools@${version} version`, { stdio: 'ignore' }); 30 | return true; 31 | } catch { 32 | return false; 33 | } 34 | } 35 | 36 | try { 37 | // Get additional arguments to pass to npm publish 38 | const args = process.argv.slice(2); 39 | const tagIndex = args.indexOf('--tag'); 40 | const tag = tagIndex !== -1 ? args[tagIndex + 1] : null; 41 | const publishArgs = args.join(' '); 42 | 43 | // Validate that --tag is set to either alpha or latest 44 | if (!tag || !['alpha', 'latest'].includes(tag)) { 45 | throw new Error('--tag must be set to either "alpha" or "latest"'); 46 | } 47 | 48 | // Verify stagehand script matches 49 | console.log('\nVerifying stagehand script...'); 50 | verifyStagehandScript(); 51 | 52 | // Run lint and build 53 | run('npm run lint'); 54 | run('npm run build'); 55 | 56 | // Get version from package.json 57 | const version = getVersion(); 58 | 59 | // Stage package.json changes 60 | run('git add package.json'); 61 | 62 | // Only commit if there are staged changes 63 | if (hasGitChanges()) { 64 | run(`git commit -m "release: v${version}"`); 65 | } 66 | 67 | // Check if version is already published 68 | if (isVersionPublished(version)) { 69 | if (tag) { 70 | // If version exists and tag is specified, just update the tag 71 | console.log(`Version ${version} already exists, updating tag...`); 72 | run(`npm dist-tag add vibe-tools@${version} ${tag}`); 73 | } else { 74 | throw new Error(`Version ${version} is already published. Please increment the version number or specify a tag to update.`); 75 | } 76 | } else { 77 | // Publish new version to npm with any additional arguments 78 | run(`npm publish ${publishArgs}`); 79 | } 80 | 81 | // Push to GitHub 82 | run('git push'); 83 | 84 | console.log(`\n✨ Successfully released v${version}`); 85 | } catch (error) { 86 | console.error('\n🚨 Release failed:', error.message); 87 | process.exit(1); 88 | } 89 | -------------------------------------------------------------------------------- /scripts/test-asset-handling.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Manual test script for asset handling in the test framework 3 | * 4 | * This script simulates the asset handling functionality of the test framework 5 | * without relying on the full test execution process. 6 | */ 7 | 8 | import path from 'path'; 9 | import fs from 'fs'; 10 | import { TestEnvironmentManager } from '../src/commands/test/environment'; 11 | 12 | async function testAssetHandling() { 13 | console.log('Testing asset handling functionality...'); 14 | 15 | // Create a mock scenario with a path reference 16 | const mockScenario = { 17 | id: 'test/test-asset-handling/1', 18 | title: 'Path Reference Asset Handling', 19 | taskDescription: 'This is a test scenario with a path reference: {{path:sample-asset.txt}}', 20 | expectedBehavior: ['Asset should be copied', 'Path reference should be replaced'], 21 | successCriteria: ['Asset exists in temp dir', 'Path reference is replaced with absolute path'] 22 | }; 23 | 24 | // Create a temporary directory 25 | const tempDir = await TestEnvironmentManager.createTempDirectory(mockScenario.id); 26 | console.log(`Created temporary directory: ${tempDir}`); 27 | 28 | try { 29 | // Copy assets and update task description with new references 30 | console.log('Original task description:', mockScenario.taskDescription); 31 | const modifiedTaskDescription = await TestEnvironmentManager.copyAssets(mockScenario, tempDir, true); 32 | console.log('Modified task description:', modifiedTaskDescription); 33 | 34 | // Check if the asset was copied 35 | const assetsTempDir = path.join(tempDir, 'assets'); 36 | const files = await fs.promises.readdir(assetsTempDir); 37 | console.log(`Files in assets directory: ${files.join(', ')}`); 38 | 39 | // Check if the file exists 40 | const assetFile = files.find(file => file.includes('sample-asset')); 41 | if (assetFile) { 42 | console.log(`Asset file found: ${assetFile}`); 43 | const assetPath = path.join(assetsTempDir, assetFile); 44 | const content = await fs.promises.readFile(assetPath, 'utf-8'); 45 | console.log(`Asset content: ${content}`); 46 | } else { 47 | console.error('Asset file not found in the temporary directory'); 48 | } 49 | 50 | // Check if the path reference was replaced 51 | if (modifiedTaskDescription.includes('{{path:sample-asset.txt}}')) { 52 | console.error('Path reference was not replaced in the task description'); 53 | } else if (modifiedTaskDescription.includes(tempDir)) { 54 | console.log('Path reference was successfully replaced with the absolute path'); 55 | } else { 56 | console.error('Path reference was replaced, but not with the expected absolute path'); 57 | } 58 | 59 | console.log('Test completed successfully!'); 60 | } catch (error) { 61 | console.error('Error during test:', error); 62 | } finally { 63 | // Clean up the temporary directory 64 | await TestEnvironmentManager.cleanup(tempDir); 65 | console.log(`Cleaned up temporary directory: ${tempDir}`); 66 | } 67 | } 68 | 69 | // Run the test 70 | testAssetHandling().catch(error => { 71 | console.error('Test failed:', error); 72 | process.exit(1); 73 | }); 74 | -------------------------------------------------------------------------------- /scripts/verify-stagehand.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node, commonjs */ 2 | const { readFileSync } = require('fs'); 3 | const { resolve } = require('path'); 4 | 5 | function verifyStagehandScript() { 6 | // Read our bundled script 7 | const bundledScriptPath = resolve(__dirname, '../src/commands/browser/stagehand/stagehandScript.ts'); 8 | const bundledContent = readFileSync(bundledScriptPath, 'utf8'); 9 | 10 | // Extract the actual script content from our bundled version (removing the export and comment) 11 | const scriptMatch = bundledContent.match(/export const STAGEHAND_SCRIPT = "([\s\S]*)";/); 12 | if (!scriptMatch) { 13 | throw new Error('Could not find STAGEHAND_SCRIPT in bundled file'); 14 | } 15 | const bundledScript = scriptMatch[1]; 16 | 17 | // Read the original script from node_modules 18 | const originalScriptPath = resolve(__dirname, '../node_modules/@browserbasehq/stagehand/dist/lib/dom/build/scriptContent.d.ts'); 19 | const originalScript = readFileSync(originalScriptPath, 'utf8'); 20 | // export const scriptContent = 21 | const originalScriptContentMatch = originalScript.match(/export declare const scriptContent = "([\s\S]*)";/); 22 | if (!originalScriptContentMatch) { 23 | throw new Error('Could not find scriptContent in original script'); 24 | } 25 | const originalScriptContent = originalScriptContentMatch[1]; 26 | 27 | // Compare the scripts 28 | if (bundledScript.trim() !== originalScriptContent.trim()) { 29 | throw new Error( 30 | 'Stagehand script mismatch detected!\n' + 31 | 'The bundled script in src/commands/browser/stagehand/stagehandScript.ts does not match\n' + 32 | 'the script in node_modules/@browserbasehq/stagehand/lib/dom/build/scriptContent.ts\n\n' + 33 | 'Please update the bundled script to match the latest version.' 34 | ); 35 | } 36 | 37 | console.log('✓ Stagehand script verification passed'); 38 | } 39 | 40 | // If this script is run directly 41 | if (require.main === module) { 42 | verifyStagehandScript(); 43 | } 44 | 45 | module.exports = { verifyStagehandScript }; 46 | -------------------------------------------------------------------------------- /src/commands/browser/browserCommand.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandGenerator, CommandOptions, CommandMap } from '../../types'; 2 | import { loadEnv } from '../../config'; 3 | import { OpenCommand } from './open.ts'; 4 | import { ElementCommand } from './element.ts'; 5 | import { ActCommand } from './stagehand/act.ts'; 6 | import { ExtractCommand } from './stagehand/extract.ts'; 7 | import { ObserveCommand } from './stagehand/observe.ts'; 8 | 9 | export class BrowserCommand implements Command { 10 | private subcommands: CommandMap = { 11 | open: new OpenCommand(), 12 | element: new ElementCommand(), 13 | act: new ActCommand(), 14 | extract: new ExtractCommand(), 15 | observe: new ObserveCommand(), 16 | }; 17 | 18 | async *execute(query: string, options: CommandOptions): CommandGenerator { 19 | loadEnv(); 20 | 21 | const [subcommand, ...rest] = query.split(' '); 22 | const subQuery = rest.join(' '); 23 | 24 | if (!subcommand) { 25 | yield 'Please specify a browser subcommand: open, element, act, extract, observe'; 26 | return; 27 | } 28 | 29 | const subCommandHandler = this.subcommands[subcommand]; 30 | try { 31 | if (subCommandHandler) { 32 | yield* subCommandHandler.execute(subQuery, options); 33 | } else { 34 | yield `Unknown browser subcommand: ${subcommand}. Available subcommands: open, element, act, extract, observe`; 35 | } 36 | } catch (error) { 37 | console.error('Error executing browser command', error); 38 | throw error; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/browser/browserOptions.ts: -------------------------------------------------------------------------------- 1 | import type { CommandOptions } from '../../types'; 2 | 3 | /** 4 | * Options that are shared across all browser commands 5 | */ 6 | export interface SharedBrowserCommandOptions extends CommandOptions { 7 | /** URL to navigate to */ 8 | url?: string; 9 | /** Global timeout for browser operations in milliseconds */ 10 | timeout?: number; 11 | /** Whether to run browser in headless mode */ 12 | headless?: boolean; 13 | /** Whether to capture and display HTML content */ 14 | html?: boolean; 15 | /** Path to save screenshot to */ 16 | screenshot?: string; 17 | /** Whether to capture and display console messages */ 18 | console?: boolean; 19 | /** Whether to capture and display network activity */ 20 | network?: boolean; 21 | /** Directory to save video recordings to */ 22 | video?: string; 23 | /** JavaScript code to execute in the browser before the main command */ 24 | evaluate?: string; 25 | /** Viewport size in format "widthxheight" (e.g. "1280x720") */ 26 | viewport?: string; 27 | /** Port number to connect to existing Chrome instance */ 28 | connectTo?: number; 29 | } 30 | 31 | /** 32 | * Options specific to the browser open command 33 | */ 34 | export interface OpenCommandOptions extends SharedBrowserCommandOptions { 35 | /** Wait condition after page load (time duration or CSS selector) */ 36 | wait?: string; 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/browser/element.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandGenerator, CommandOptions } from '../../types'; 2 | import { chromium } from 'playwright'; 3 | import { loadConfig } from '../../config.ts'; 4 | import { ensurePlaywright } from './utils.ts'; 5 | import type { SharedBrowserCommandOptions } from './browserOptions'; 6 | import { setupConsoleLogging, setupNetworkMonitoring, outputMessages } from './utilsShared'; 7 | 8 | interface ElementBrowserOptions extends SharedBrowserCommandOptions { 9 | selector?: string; 10 | text?: boolean; 11 | } 12 | 13 | export class ElementCommand implements Command { 14 | private config = loadConfig(); 15 | 16 | async *execute(query: string, options: ElementBrowserOptions): CommandGenerator { 17 | let browser; 18 | let consoleMessages: string[] = []; 19 | let networkMessages: string[] = []; 20 | 21 | // Set default options 22 | options = { 23 | ...options, 24 | network: options?.network === undefined ? true : options.network, 25 | console: options?.console === undefined ? true : options.console, 26 | }; 27 | 28 | try { 29 | // Check for Playwright availability first 30 | await ensurePlaywright(); 31 | 32 | // Parse selector from query if not provided in options 33 | if (!options?.selector && query) { 34 | if (options?.url) { 35 | options = { ...options, selector: query }; 36 | } else { 37 | const parts = query.split(' '); 38 | if (parts.length >= 2) { 39 | const url = parts[0]; 40 | const selector = parts.slice(1).join(' '); 41 | options = { ...options, url, selector }; 42 | } else { 43 | yield 'Please provide both URL and selector. Usage: vibe-tools browser element [options]'; 44 | return; 45 | } 46 | } 47 | } 48 | 49 | if (!options?.url || !options?.selector) { 50 | yield 'Please provide both URL and selector. Usage: vibe-tools browser element [options]'; 51 | return; 52 | } 53 | 54 | const browserType = chromium; 55 | yield 'Launching browser...'; 56 | browser = await browserType.launch({ 57 | headless: true, // Always headless for element inspection 58 | }); 59 | const page = await browser.newPage(); 60 | 61 | // Setup console and network monitoring 62 | consoleMessages = await setupConsoleLogging(page, options); 63 | networkMessages = await setupNetworkMonitoring(page, options); 64 | 65 | yield `Navigating to ${options.url}...`; 66 | await page.goto(options.url, { timeout: this.config.browser?.timeout ?? 30000 }); 67 | 68 | yield `Finding element with selector "${options.selector}"...`; 69 | const element = await page.$(options.selector); 70 | 71 | if (!element) { 72 | yield `Element with selector "${options.selector}" not found on the page.`; 73 | return; 74 | } 75 | 76 | if (options.html === true) { 77 | const elementHTML = await element.innerHTML(); 78 | yield '\n--- Element HTML Content ---\n\n'; 79 | yield elementHTML; 80 | yield '\n--- End of Element HTML Content ---\n'; 81 | } 82 | 83 | if (options.text === true) { 84 | const elementText = await element.textContent(); 85 | yield '\n--- Element Text Content ---\n\n'; 86 | yield elementText?.trim() || ''; 87 | yield '\n--- End of Element Text Content ---\n'; 88 | } 89 | 90 | if (options.screenshot) { 91 | yield `Taking screenshot of element and saving to ${options.screenshot}...`; 92 | await element.screenshot({ path: options.screenshot }); 93 | yield 'Element screenshot saved.\n'; 94 | } 95 | 96 | // If no output options specified, show both HTML and text by default 97 | if (options.html === undefined && options.text === undefined && !options.screenshot) { 98 | const elementHTML = await element.innerHTML(); 99 | const elementText = await element.textContent(); 100 | 101 | yield '\n--- Element Content ---\n\n'; 102 | yield 'HTML:\n'; 103 | yield elementHTML; 104 | yield '\n\nText:\n'; 105 | yield elementText?.trim() || ''; 106 | yield '\n--- End of Element Content ---\n'; 107 | } 108 | 109 | // Output console and network messages 110 | for (const message of outputMessages(consoleMessages, networkMessages, options)) { 111 | yield message; 112 | } 113 | } catch (error) { 114 | yield `Browser element command error: ${error instanceof Error ? error.message : 'Unknown error'}`; 115 | } finally { 116 | if (browser) { 117 | await browser.close(); 118 | yield 'Browser closed.\n'; 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/commands/browser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './browserCommand'; 2 | export * from './open'; 3 | export * from './element'; 4 | -------------------------------------------------------------------------------- /src/commands/browser/stagehand/scriptContent.ts: -------------------------------------------------------------------------------- 1 | import { STAGEHAND_SCRIPT } from './stagehandScript'; 2 | import { once } from '../../../utils/once'; 3 | 4 | export const scriptContent = once(() => STAGEHAND_SCRIPT); 5 | -------------------------------------------------------------------------------- /src/commands/browser/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if Playwright is available and returns its version information 3 | * @returns Object containing availability status and version info 4 | */ 5 | export async function checkPlaywright(): Promise<{ 6 | available: boolean; 7 | version?: string; 8 | error?: string; 9 | }> { 10 | try { 11 | // Try to dynamically import playwright 12 | const playwright = await import('playwright'); 13 | if (!playwright) { 14 | return { available: false, error: 'Playwright package not found' }; 15 | } 16 | 17 | // Get Playwright version by checking if it's installed 18 | try { 19 | // This will throw if playwright is not installed 20 | await import('playwright'); 21 | return { available: true, version: 'installed' }; 22 | } catch (importError) { 23 | if (importError instanceof Error) { 24 | if (importError.message.includes('Cannot find package')) { 25 | return { 26 | available: false, 27 | error: 'Playwright is not installed. Please install it using: npm install playwright', 28 | }; 29 | } 30 | return { available: false, error: importError.message }; 31 | } 32 | return { available: false, error: 'Unknown error while importing Playwright' }; 33 | } 34 | } catch (error) { 35 | // Handle different types of errors 36 | if (error instanceof Error) { 37 | if (error.message.includes('Cannot find package')) { 38 | return { 39 | available: false, 40 | error: 'Playwright is not installed. Please install it using: npm install playwright', 41 | }; 42 | } 43 | return { available: false, error: error.message }; 44 | } 45 | return { available: false, error: 'Unknown error while checking Playwright' }; 46 | } 47 | } 48 | 49 | /** 50 | * Ensures Playwright is available before proceeding 51 | * @returns true if Playwright is available, throws error if not 52 | */ 53 | export async function ensurePlaywright(): Promise { 54 | const { available, version, error } = await checkPlaywright(); 55 | 56 | if (!available) { 57 | throw new Error( 58 | `Playwright is required for browser commands but is not available.\n` + 59 | `Error: ${error}\n` + 60 | `Please install Playwright using one of these commands:\n` + 61 | ` npm install playwright\n` + 62 | ` yarn add playwright\n` + 63 | ` pnpm add playwright` 64 | ); 65 | } 66 | 67 | if (version) { 68 | console.log(`Using Playwright: ${version}`); 69 | } 70 | return true; 71 | } 72 | -------------------------------------------------------------------------------- /src/commands/clickup.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandGenerator, CommandOptions, CommandMap } from '../types'; 2 | import { TaskCommand } from './clickup/task'; 3 | 4 | export class ClickUpCommand implements Command { 5 | private subcommands: CommandMap = { 6 | task: new TaskCommand(), 7 | }; 8 | 9 | async *execute(query: string, options: CommandOptions): CommandGenerator { 10 | const [subcommand, ...rest] = query.split(' '); 11 | const subQuery = rest.join(' '); 12 | 13 | if (!subcommand) { 14 | yield 'Please specify a subcommand: task'; 15 | return; 16 | } 17 | 18 | if (this.subcommands[subcommand]) { 19 | yield* this.subcommands[subcommand].execute(subQuery, options); 20 | } else { 21 | yield `Unknown subcommand: ${subcommand}. Available subcommands: task`; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/clickup/clickupAuth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get ClickUp API token from environment 3 | */ 4 | export function getClickUpToken(): string | undefined { 5 | return process.env.CLICKUP_API_TOKEN; 6 | } 7 | 8 | /** 9 | * Get ClickUp authentication headers for API requests 10 | */ 11 | export function getClickUpHeaders(): Record { 12 | const token = getClickUpToken(); 13 | if (!token) { 14 | throw new Error( 15 | 'CLICKUP_API_TOKEN environment variable is not set. Please set it in your .vibe-tools.env file.' 16 | ); 17 | } 18 | 19 | return { 20 | Authorization: token, 21 | 'Content-Type': 'application/json', 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/clickup/task.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandGenerator } from '../../types'; 2 | import { loadEnv } from '../../config'; 3 | import { getClickUpHeaders } from './clickupAuth'; 4 | import { formatDate, formatStatus, type ClickUpOptions } from './utils'; 5 | 6 | export class TaskCommand implements Command { 7 | constructor() { 8 | loadEnv(); 9 | } 10 | 11 | private async fetchComments(taskId: string): Promise { 12 | const url = `https://api.clickup.com/api/v2/task/${taskId}/comment`; 13 | try { 14 | const response = await fetch(url, { 15 | headers: getClickUpHeaders(), 16 | }); 17 | 18 | if (!response.ok) { 19 | return []; 20 | } 21 | 22 | return await response.json(); 23 | } catch (error) { 24 | console.error('Error fetching comments:', error); 25 | return []; 26 | } 27 | } 28 | 29 | async *execute(query: string, options?: ClickUpOptions): CommandGenerator { 30 | const taskId = query.trim(); 31 | 32 | if (!taskId) { 33 | yield 'Please specify a task ID (e.g., vibe-tools clickup task "task_id")'; 34 | return; 35 | } 36 | 37 | try { 38 | const response = await fetch(`https://api.clickup.com/api/v2/task/${taskId}`, { 39 | headers: getClickUpHeaders(), 40 | }); 41 | 42 | if (!response.ok) { 43 | yield `ClickUp API Error: ${response.status} - ${response.statusText}`; 44 | if (response.status === 404) { 45 | yield ` (Task ${taskId} not found)`; 46 | } else if (response.status === 401) { 47 | yield '\nAuthentication failed. Please check your CLICKUP_API_TOKEN.'; 48 | } 49 | return; 50 | } 51 | 52 | const task = await response.json(); 53 | 54 | // Task header 55 | yield `## Task\n`; 56 | yield `[${task.id}] ${task.name}\n`; 57 | yield `Status: ${formatStatus(task.status)}\n`; 58 | yield `URL: ${task.url}\n\n`; 59 | 60 | // Description 61 | yield `## Description\n`; 62 | yield `${task.description || 'No description provided.'}\n\n`; 63 | 64 | // Task metadata 65 | yield `Created by: ${task.creator.username}\n`; 66 | yield `Created on: ${formatDate(task.date_created)}\n`; 67 | if (task.date_updated) { 68 | yield `Updated on: ${formatDate(task.date_updated)}\n`; 69 | } 70 | if (task.date_closed) { 71 | yield `Closed on: ${formatDate(task.date_closed)}\n`; 72 | } 73 | 74 | // Comments 75 | const commentsRes = await this.fetchComments(taskId); 76 | if ( 77 | 'comments' in commentsRes && 78 | Array.isArray(commentsRes.comments) && 79 | commentsRes.comments.length > 0 80 | ) { 81 | const comments = commentsRes.comments; 82 | yield `\n## Comments, newest to oldest (${comments.length})\n\n`; 83 | for (const comment of comments) { 84 | yield `**@${comment.user.username}** commented on ${formatDate(comment.date)}\n`; 85 | yield `> ${comment.comment_text || 'No content'}\n`; 86 | } 87 | } 88 | 89 | // Additional metadata 90 | yield `---\n`; 91 | if (task.assignees?.length > 0) { 92 | yield `Assignees: ${task.assignees.map((a: any) => '@' + a.username).join(', ')}\n`; 93 | } 94 | if (task.tags?.length > 0) { 95 | yield `Tags: ${task.tags.map((t: any) => t.name).join(', ')}\n`; 96 | } 97 | if (task.priority) { 98 | yield `Priority: ${task.priority.priority}\n`; 99 | } 100 | if (task.due_date) { 101 | yield `Due date: ${formatDate(task.due_date)}\n`; 102 | } 103 | } catch (error) { 104 | yield `Error fetching task: ${error instanceof Error ? error.message : 'Unknown error'}`; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/commands/clickup/utils.ts: -------------------------------------------------------------------------------- 1 | import type { CommandOptions } from '../../types'; 2 | 3 | export type ClickUpOptions = CommandOptions; 4 | 5 | export function formatDate(timestamp: unknown): string { 6 | return new Date(Number(timestamp)).toLocaleString(); 7 | } 8 | 9 | export function formatStatus(status: { status: string; color: string }): string { 10 | return `${status.status} (${status.color})`; 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/github.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandGenerator, CommandOptions, CommandMap } from '../types'; 2 | import { PrCommand } from './github/pr.ts'; 3 | import { IssueCommand } from './github/issue.ts'; 4 | 5 | export class GithubCommand implements Command { 6 | private subcommands: CommandMap = { 7 | pr: new PrCommand(), 8 | issue: new IssueCommand(), 9 | }; 10 | 11 | async *execute(query: string, options: CommandOptions): CommandGenerator { 12 | const [subcommand, ...rest] = query.split(' '); 13 | const subQuery = rest.join(' '); 14 | 15 | if (!subcommand) { 16 | yield 'Please specify a subcommand: pr or issue'; 17 | return; 18 | } 19 | 20 | if (this.subcommands[subcommand]) { 21 | yield* this.subcommands[subcommand].execute(subQuery, options); 22 | } else { 23 | yield `Unknown subcommand: ${subcommand}. Available subcommands: pr, issue`; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/github/githubAuth.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | 3 | /** 4 | * Check if the GitHub CLI is available and logged in 5 | */ 6 | export function isGitHubCliAvailable(): boolean { 7 | try { 8 | // Check if gh is installed 9 | execSync('command -v gh', { stdio: 'ignore' }); 10 | 11 | // Check if gh is logged in 12 | execSync('gh auth status', { stdio: 'ignore' }); 13 | 14 | return true; 15 | } catch { 16 | return false; 17 | } 18 | } 19 | 20 | /** 21 | * Try to get GitHub credentials from git credential helper 22 | * Returns undefined if no credentials are found or if there's an error 23 | */ 24 | export function getGitCredentials(): { username: string; password: string } | undefined { 25 | try { 26 | // Prepare input for git credential fill 27 | const input = 'protocol=https\nhost=github.com\n\n'; 28 | 29 | // Run git credential fill 30 | const output = execSync('git credential fill', { 31 | input, 32 | encoding: 'utf8', 33 | stdio: ['pipe', 'pipe', 'ignore'], // Ignore stderr to prevent warning messages 34 | }); 35 | 36 | // Parse the output 37 | const lines = output.split('\n'); 38 | const credentials: { [key: string]: string } = {}; 39 | 40 | for (const line of lines) { 41 | const [key, value] = line.split('='); 42 | if (key && value) { 43 | credentials[key.trim()] = value.trim(); 44 | } 45 | } 46 | 47 | // Check if we have both username and password 48 | if (credentials.username && credentials.password) { 49 | return { 50 | username: credentials.username, 51 | password: credentials.password, 52 | }; 53 | } 54 | 55 | return undefined; 56 | } catch { 57 | return undefined; 58 | } 59 | } 60 | 61 | let ghAuthLoginMessagePrinted = false; 62 | /** 63 | * Try to get a GitHub token using various methods: 64 | * 1. Check environment variable 65 | * 2. If GitHub CLI is available and logged in, generate a token 66 | * 3. Try git credentials if available 67 | * 68 | * @returns The GitHub token if available, undefined otherwise 69 | */ 70 | export function getGitHubToken(): string | undefined { 71 | // First check environment variable 72 | if (process.env.GITHUB_TOKEN) { 73 | return process.env.GITHUB_TOKEN; 74 | } 75 | 76 | // Then try GitHub CLI 77 | if (isGitHubCliAvailable()) { 78 | try { 79 | // Generate a token with necessary scopes for PR/Issue operations 80 | const token = execSync('gh auth token', { 81 | encoding: 'utf8', 82 | stdio: ['ignore', 'pipe', 'ignore'], // Ignore stderr to prevent warning messages 83 | }).trim(); 84 | 85 | return token; 86 | } catch { 87 | if (!ghAuthLoginMessagePrinted) { 88 | console.error( 89 | 'Failed to generate GitHub token using gh CLI. Run `gh auth login` to login to GitHub CLI.' 90 | ); 91 | ghAuthLoginMessagePrinted = true; 92 | } 93 | } 94 | } 95 | 96 | // Finally, try git credentials 97 | const credentials = getGitCredentials(); 98 | if (credentials) { 99 | // If the password looks like a token (starts with 'ghp_' or 'gho_'), use it directly 100 | if (credentials.password.startsWith('ghp_') || credentials.password.startsWith('gho_')) { 101 | return credentials.password; 102 | } 103 | } 104 | 105 | return undefined; 106 | } 107 | 108 | /** 109 | * Get GitHub authentication headers for API requests 110 | */ 111 | export function getGitHubHeaders(): Record { 112 | const headers: Record = { 113 | Accept: 'application/vnd.github.v3+json', 114 | }; 115 | 116 | // First try to get a token 117 | const token = getGitHubToken(); 118 | if (token) { 119 | headers['Authorization'] = `token ${token}`; 120 | return headers; 121 | } 122 | 123 | // If no token, try git credentials for Basic Auth 124 | const credentials = getGitCredentials(); 125 | if (credentials) { 126 | const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64'); 127 | headers['Authorization'] = `Basic ${auth}`; 128 | } 129 | 130 | return headers; 131 | } 132 | -------------------------------------------------------------------------------- /src/commands/github/issue.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandGenerator } from '../../types.ts'; 2 | import { loadEnv } from '../../config.ts'; 3 | import { getRepoContext, type GithubOptions } from './utils.ts'; 4 | import { getGitHubHeaders, isGitHubCliAvailable, getGitCredentials } from './githubAuth.ts'; 5 | 6 | export class IssueCommand implements Command { 7 | constructor() { 8 | loadEnv(); // Load environment variables for potential future API key needs 9 | } 10 | 11 | private async fetchComments(owner: string, repo: string, issueNumber: number): Promise { 12 | const url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`; 13 | try { 14 | const response = await fetch(url, { 15 | headers: getGitHubHeaders(), 16 | }); 17 | 18 | if (!response.ok) { 19 | return []; 20 | } 21 | 22 | return await response.json(); 23 | } catch (error) { 24 | console.error('Error fetching comments:', error); 25 | return []; 26 | } 27 | } 28 | 29 | private formatDate(dateStr: string): string { 30 | return new Date(dateStr).toLocaleString(); 31 | } 32 | 33 | async *execute(query: string, options?: GithubOptions): CommandGenerator { 34 | const repoContext = await getRepoContext(options); 35 | if (!repoContext) { 36 | yield 'Could not determine repository context. Please run this command inside a GitHub repository, or specify the repository with --from-github owner/repo or --repo owner/repo.'; 37 | return; 38 | } 39 | const { owner, repo } = repoContext; 40 | 41 | // Check if we have GitHub authentication 42 | const credentials = getGitCredentials(); 43 | if (!process.env.GITHUB_TOKEN && !isGitHubCliAvailable() && !credentials) { 44 | yield 'Note: No GitHub authentication found. Using unauthenticated access (rate limits apply).\n'; 45 | yield 'To increase rate limits, either:\n'; 46 | yield '1. Set GITHUB_TOKEN in your environment\n'; 47 | yield '2. Install and login to GitHub CLI (gh)\n'; 48 | yield '3. Configure git credentials for github.com\n\n'; 49 | } 50 | 51 | const issueNumber = parseInt(query, 10); // Try to parse an issue number 52 | 53 | let url: string; 54 | if (isNaN(issueNumber)) { 55 | url = `https://api.github.com/repos/${owner}/${repo}/issues?state=open&sort=created&direction=desc&per_page=10`; 56 | } else { 57 | url = `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`; 58 | } 59 | 60 | try { 61 | const response = await fetch(url, { 62 | headers: getGitHubHeaders(), 63 | }); 64 | 65 | if (!response.ok) { 66 | yield `GitHub API Error: ${response.status} - ${response.statusText}`; 67 | if (response.status === 404) { 68 | yield ` (Issue ${issueNumber} not found or repository is private without authentication)`; 69 | } else if ( 70 | response.status === 403 && 71 | response.headers.get('x-ratelimit-remaining') === '0' 72 | ) { 73 | yield '\nRate limit exceeded. To increase rate limits, either:\n'; 74 | yield '1. Set GITHUB_TOKEN in your environment\n'; 75 | yield '2. Install and login to GitHub CLI (gh)\n'; 76 | } 77 | return; 78 | } 79 | 80 | const data = await response.json(); 81 | 82 | if (isNaN(issueNumber)) { 83 | // Listing issues 84 | if (data.length === 0) { 85 | yield 'No open issues found.'; 86 | return; 87 | } 88 | for (const issue of data) { 89 | yield `#${issue.number}: ${issue.title} by ${issue.user.login} (${issue.html_url})\n`; 90 | } 91 | } else { 92 | // Single issue with full discussion 93 | const issue = data; 94 | 95 | // Issue header 96 | yield `#${issue.number}: ${issue.title}\n`; 97 | yield `State: ${issue.state}\n`; 98 | yield `URL: ${issue.html_url}\n\n`; 99 | 100 | // Original post 101 | yield `## Original Post\n`; 102 | yield `**@${issue.user.login}** opened this issue on ${this.formatDate(issue.created_at)}\n\n`; 103 | yield `${issue.body || 'No description provided.'}\n\n`; 104 | 105 | // Comments 106 | const comments = await this.fetchComments(owner, repo, issueNumber); 107 | if (comments.length > 0) { 108 | yield `## Discussion (${comments.length} comments)\n`; 109 | for (const comment of comments) { 110 | yield `\n---\n`; 111 | yield `**@${comment.user.login}** commented on ${this.formatDate(comment.created_at)}\n\n`; 112 | yield `${comment.body || 'No content'}\n`; 113 | } 114 | } else { 115 | yield `\nNo comments yet.\n`; 116 | } 117 | 118 | // Issue metadata 119 | yield `\n---\n`; 120 | yield `Labels: ${issue.labels.map((l: any) => l.name).join(', ') || 'None'}\n`; 121 | if (issue.assignees?.length > 0) { 122 | yield `Assignees: ${issue.assignees.map((a: any) => '@' + a.login).join(', ')}\n`; 123 | } 124 | if (issue.milestone) { 125 | yield `Milestone: ${issue.milestone.title}\n`; 126 | } 127 | } 128 | } catch (error) { 129 | yield `Error fetching issues: ${error instanceof Error ? error.message : 'Unknown error'}`; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/commands/github/utils.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import type { CommandOptions } from '../../types'; 3 | 4 | export interface GithubOptions extends CommandOptions { 5 | repo?: string; 6 | fromGithub?: string; 7 | } 8 | 9 | export interface RepoContext { 10 | owner: string; 11 | repo: string; 12 | } 13 | 14 | function parseRepoString(repoStr: string): RepoContext | null { 15 | const parts = repoStr.split('/'); 16 | if (parts.length === 2) { 17 | return { owner: parts[0], repo: parts[1] }; 18 | } else { 19 | console.error("Invalid repository format. Use 'owner/repo'"); 20 | return null; 21 | } 22 | } 23 | 24 | export async function getRepoContext(options?: GithubOptions): Promise { 25 | // First try --from-github for consistency with other commands 26 | if (options?.fromGithub) { 27 | return parseRepoString(options.fromGithub); 28 | } 29 | 30 | // Then try --repo for backward compatibility 31 | if (options?.repo) { 32 | return parseRepoString(options.repo); 33 | } 34 | 35 | try { 36 | // Finally, try to get the remote URL from git (works if inside a git repo) 37 | const remoteUrl = execSync('git config --get remote.origin.url', { stdio: 'pipe' }) 38 | .toString() 39 | .trim(); 40 | 41 | // Extract owner and repo name from the URL. Handles both SSH and HTTPS URLs 42 | const match = remoteUrl.match(/github\.com[:/](.*?)\/(.*?)(?:\.git)?$/); 43 | if (match) { 44 | return { owner: match[1], repo: match[2] }; 45 | } 46 | } catch { 47 | // Not a git repository, or git command failed 48 | return null; 49 | } 50 | return null; 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | import type { CommandMap } from '../types.ts'; 2 | import { WebCommand } from './web.ts'; 3 | import { InstallCommand } from './install.ts'; 4 | import { JsonInstallCommand } from './jsonInstall.ts'; 5 | import { GithubCommand } from './github.ts'; 6 | import { BrowserCommand } from './browser/browserCommand.ts'; 7 | import { PlanCommand } from './plan.ts'; 8 | import { RepoCommand } from './repo.ts'; 9 | import { DocCommand } from './doc.ts'; 10 | import { AskCommand } from './ask.ts'; 11 | import { MCPCommand } from './mcp/mcp.ts'; 12 | import { XcodeCommand } from './xcode/xcode.ts'; 13 | import { ClickUpCommand } from './clickup.ts'; 14 | import TestCommand from './test/index.ts'; 15 | import YouTubeCommand from './youtube/index.ts'; 16 | import { WaitCommand } from './wait.ts'; 17 | 18 | export const commands: CommandMap = { 19 | web: new WebCommand(), 20 | repo: new RepoCommand(), 21 | install: new InstallCommand(), 22 | doc: new DocCommand(), 23 | github: new GithubCommand(), 24 | browser: new BrowserCommand(), 25 | plan: new PlanCommand(), 26 | ask: new AskCommand(), 27 | mcp: new MCPCommand(), 28 | xcode: new XcodeCommand(), 29 | clickup: new ClickUpCommand(), 30 | test: new TestCommand(), 31 | youtube: new YouTubeCommand(), 32 | wait: new WaitCommand(), 33 | }; 34 | -------------------------------------------------------------------------------- /src/commands/mcp/client/MCPClientNew.ts: -------------------------------------------------------------------------------- 1 | import { UnifiedLLMClient } from '../../../utils/tool-enabled-llm/unified-client.js'; 2 | import { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js'; 3 | import { MCPError, handleMCPError } from './errors.js'; 4 | 5 | // InternalMessage interface - matches the one in unified-client.js 6 | export interface InternalMessage { 7 | role: 'user' | 'assistant' | 'tool'; 8 | content: string | any[]; 9 | tool_call_id?: string; 10 | name?: string; 11 | cache_control?: { type: string }; 12 | } 13 | 14 | export interface MCPClientOptions extends StdioServerParameters { 15 | provider: 'anthropic' | 'openrouter'; 16 | } 17 | 18 | const SYSTEM_PROMPT = `You are a helpful AI assistant using tools. 19 | When you receive a tool result, do not call the same tool again with the same arguments unless the user explicitly asks for it or the context changes significantly. 20 | Use the results provided by the tools to answer the user's query. 21 | If you have already called a tool with the same arguments and received a result, reuse the result instead of calling the tool again. 22 | When you receive a tool result, focus on interpreting and explaining the result to the user rather than making additional tool calls.`; 23 | 24 | export class MCPClientNew { 25 | private unifiedClient: UnifiedLLMClient; 26 | public config: MCPClientOptions; 27 | 28 | constructor( 29 | serverConfig: MCPClientOptions & { model: string; maxTokens?: number }, 30 | private debug: boolean 31 | ) { 32 | this.config = serverConfig; 33 | const provider = serverConfig.provider; 34 | const model = serverConfig.model; 35 | // Initialize the unified client 36 | this.unifiedClient = new UnifiedLLMClient({ 37 | provider: provider, 38 | debug: debug, 39 | mcpMode: true, 40 | mcpConfig: serverConfig, 41 | model, 42 | maxTokens: serverConfig.maxTokens || 8192, 43 | logger: (message) => { 44 | console.log(message); 45 | }, 46 | }); 47 | } 48 | 49 | async start() { 50 | try { 51 | console.log('starting mcp client'); 52 | 53 | // Start the MCP mode in the unified client 54 | await this.unifiedClient.startMCP(); 55 | } catch (error) { 56 | console.error('Failed to initialize MCP Client:', error); 57 | const mcpError = handleMCPError(error); 58 | throw mcpError; 59 | } 60 | } 61 | 62 | async stop() { 63 | try { 64 | // Stop the MCP client in unified client 65 | await this.unifiedClient.stopMCP(); 66 | } catch (error) { 67 | const mcpError = handleMCPError(error); 68 | console.error('Error closing MCP Client:', mcpError); 69 | throw mcpError; 70 | } 71 | } 72 | 73 | async processQuery(query: string) { 74 | try { 75 | // Add available variables to the query 76 | const envVars = printSafeEnvVars(); 77 | const enhancedQuery = `Available variables ${envVars}\n\n${query}`; 78 | 79 | // Process the query using the unified client 80 | const messages = await this.unifiedClient.processQuery(enhancedQuery, SYSTEM_PROMPT); 81 | 82 | // Return messages, skipping the first one which is just the env vars 83 | return messages.slice(1); 84 | } catch (error) { 85 | console.error('Error during query processing:', error); 86 | const mcpError = handleMCPError(error); 87 | console.error('\nError during query processing:', mcpError.message); 88 | if (this.debug) { 89 | console.error(mcpError.stack); 90 | } 91 | throw mcpError; 92 | } 93 | } 94 | } 95 | 96 | function printSafeEnvVars() { 97 | const envVars = Object.keys(process.env); 98 | return envVars 99 | .filter( 100 | (envVar) => 101 | !envVar.toUpperCase().includes('KEY') && 102 | !envVar.toUpperCase().includes('TOKEN') && 103 | !envVar.toUpperCase().includes('SECRET') && 104 | !envVar.toUpperCase().includes('PASSWORD') 105 | ) 106 | .map((envVar) => `${envVar}=${process.env[envVar]}`) 107 | .join('\n'); 108 | } 109 | -------------------------------------------------------------------------------- /src/commands/mcp/client/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom error classes for the MCP client 3 | */ 4 | 5 | export class MCPError extends Error { 6 | constructor(message: string) { 7 | super(message); 8 | this.name = 'MCPError'; 9 | } 10 | } 11 | 12 | export class MCPConnectionError extends MCPError { 13 | constructor(message: string) { 14 | super(message); 15 | this.name = 'MCPConnectionError'; 16 | } 17 | } 18 | 19 | export class MCPServerError extends MCPError { 20 | constructor( 21 | message: string, 22 | public code?: string 23 | ) { 24 | super(message); 25 | this.name = 'MCPServerError'; 26 | } 27 | } 28 | 29 | export class MCPToolError extends MCPError { 30 | constructor( 31 | message: string, 32 | public toolName: string 33 | ) { 34 | super(message); 35 | this.name = 'MCPToolError'; 36 | } 37 | } 38 | 39 | export class MCPConfigError extends MCPError { 40 | constructor(message: string) { 41 | super(message); 42 | this.name = 'MCPConfigError'; 43 | } 44 | } 45 | 46 | export class MCPAuthError extends MCPError { 47 | constructor(message: string) { 48 | super(message); 49 | this.name = 'MCPAuthError'; 50 | } 51 | } 52 | 53 | /** 54 | * Error handling utilities 55 | */ 56 | 57 | export function handleMCPError(error: unknown): MCPError { 58 | if (error instanceof MCPError) { 59 | return error; 60 | } 61 | 62 | if (error instanceof Error) { 63 | // Handle specific error types from the MCP SDK 64 | if (error.message.includes('ECONNREFUSED') || error.message.includes('connect ECONNREFUSED')) { 65 | return new MCPConnectionError('Could not connect to MCP server. Is the server running?'); 66 | } 67 | 68 | // Handle authentication errors 69 | if (error.message.includes('401') || error.message.includes('authentication')) { 70 | return new MCPAuthError('Authentication failed. Please check your API key.'); 71 | } 72 | 73 | // Handle server errors 74 | if (error.message.includes('500')) { 75 | return new MCPServerError('The MCP server encountered an internal error.'); 76 | } 77 | 78 | // Handle tool errors 79 | if (error.message.includes('tool not found') || error.message.includes('unknown tool')) { 80 | return new MCPToolError('The requested tool was not found on the server.', 'unknown'); 81 | } 82 | 83 | // Handle other errors 84 | return new MCPError(error.message); 85 | } 86 | 87 | // Handle unknown errors 88 | return new MCPError('An unknown error occurred'); 89 | } 90 | -------------------------------------------------------------------------------- /src/commands/mcp/client/validation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContentBlock, 3 | ContentBlockParam, 4 | ImageBlockParam, 5 | TextBlock, 6 | ToolUseBlock, 7 | } from '@anthropic-ai/sdk/resources/index.mjs'; 8 | import { z } from 'zod'; 9 | import { exhaustiveMatchGuard } from '../../../utils/exhaustiveMatchGuard'; 10 | 11 | // Schema for cache control in Anthropic message content 12 | const CacheControlSchema = z.object({ 13 | type: z.literal('ephemeral'), 14 | }); 15 | 16 | // Schema for text content in Anthropic message 17 | const TextContentSchema = z.object({ 18 | type: z.literal('text'), 19 | text: z.string(), 20 | }); 21 | 22 | // Schema for image content in Anthropic message 23 | const ImageContentSchema = z.object({ 24 | type: z.literal('image'), 25 | source: z.object({ 26 | type: z.literal('base64'), 27 | media_type: z.string().regex(/^image\/(jpeg|png|gif|webp)$/), 28 | data: z.string(), 29 | }), 30 | }); 31 | 32 | // Schema for tool use content in Anthropic message 33 | const ToolUseSchema = z.object({ 34 | type: z.literal('tool_use'), 35 | id: z.string(), 36 | name: z.string(), 37 | input: z.record(z.any()), 38 | }); 39 | 40 | // Union schema for all valid content types 41 | const ContentBlockSchema = z.union([TextContentSchema, ImageContentSchema, ToolUseSchema]); 42 | 43 | function applyOverrides( 44 | content: z.infer, 45 | overrides: { cacheControl?: 'ephemeral' } 46 | ): ContentBlockParam { 47 | if (overrides.cacheControl) { 48 | switch (content.type) { 49 | case 'text': 50 | return { 51 | type: 'text', 52 | text: content.text, 53 | cache_control: { type: overrides.cacheControl }, 54 | citations: null, 55 | } as TextBlock; 56 | case 'tool_use': 57 | return { 58 | ...content, 59 | type: 'tool_use', 60 | cache_control: { type: overrides.cacheControl }, 61 | } as ToolUseBlock; 62 | case 'image': 63 | return { 64 | ...content, 65 | type: 'image', 66 | cache_control: { type: overrides.cacheControl }, 67 | } as ImageBlockParam; 68 | default: 69 | console.error('Unknown content type', content); 70 | throw exhaustiveMatchGuard(content); 71 | } 72 | } 73 | return content as ContentBlockParam; 74 | } 75 | /** 76 | * Validates if a message content is a valid Anthropic message content object. 77 | * If the content is a string or doesn't match the expected structure, returns null. 78 | * 79 | * @param content - The message content to validate 80 | * @returns The validated content object if valid, null otherwise 81 | */ 82 | export function asValidMessageContentObject( 83 | content: unknown, 84 | overrides: { cacheControl?: 'ephemeral' } 85 | ): null | ContentBlockParam[] { 86 | if (typeof content === 'string') { 87 | return null; 88 | } 89 | 90 | // If content is an array, validate each block 91 | if (Array.isArray(content)) { 92 | const result = z.array(ContentBlockSchema).safeParse(content); 93 | return result.success ? result.data.map((block) => applyOverrides(block, overrides)) : null; 94 | } 95 | 96 | // If content is an object, try to validate it as a single content block 97 | const result = ContentBlockSchema.safeParse(content); 98 | return result.success ? [applyOverrides(result.data, overrides)] : null; 99 | } 100 | -------------------------------------------------------------------------------- /src/commands/mcp/index.ts: -------------------------------------------------------------------------------- 1 | export { mcp } from './mcp.js'; 2 | -------------------------------------------------------------------------------- /src/commands/mcp/mcp.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandGenerator, CommandOptions, CommandMap } from '../../types'; 2 | import { loadEnv, loadConfig } from '../../config.js'; 3 | import { 4 | MCPAuthError, 5 | MCPConnectionError, 6 | MCPServerError, 7 | MCPToolError, 8 | MCPConfigError, 9 | } from './client/errors.js'; 10 | import { MarketplaceManager } from './marketplace.js'; 11 | import { SearchCommand } from './search'; 12 | import { RunCommand } from './run'; 13 | 14 | // Load environment variables and config 15 | loadEnv(); 16 | 17 | export class MCPCommand implements Command { 18 | private config = loadConfig(); 19 | private marketplaceManager = new MarketplaceManager(this.config); 20 | private subcommands: CommandMap = { 21 | search: new SearchCommand(this.marketplaceManager), 22 | run: new RunCommand(this.marketplaceManager), 23 | }; 24 | 25 | async *execute(query: string, options: CommandOptions): CommandGenerator { 26 | try { 27 | // Split into subcommand and remaining query 28 | const [subcommand = 'run', ...rest] = query.split(' '); 29 | const subQuery = rest.join(' '); 30 | 31 | const subCommandHandler = this.subcommands[subcommand]; 32 | if (subCommandHandler) { 33 | yield* subCommandHandler.execute(subQuery, options); 34 | } else { 35 | yield `Unknown MCP subcommand: ${subcommand}. Available subcommands: search, run`; 36 | } 37 | } catch (error) { 38 | this.handleError(error, options?.debug); 39 | throw error; 40 | } 41 | } 42 | 43 | private handleError(error: unknown, debug?: boolean) { 44 | if (error instanceof MCPAuthError) { 45 | console.error( 46 | console.error( 47 | 'Authentication error: ' + 48 | error.message + 49 | '\nPlease check your API key in ~/.vibe-tools/.env' 50 | ) 51 | ); 52 | } else if (error instanceof MCPConnectionError) { 53 | console.error( 54 | console.error( 55 | 'Connection error: ' + 56 | error.message + 57 | '\nPlease check if the MCP server is running and accessible.' 58 | ) 59 | ); 60 | } else if (error instanceof MCPServerError) { 61 | console.error( 62 | console.error( 63 | 'Server error: ' + 64 | error.message + 65 | (error.code ? ` (Code: ${error.code})` : '') + 66 | '\nPlease try again later or contact support if the issue persists.' 67 | ) 68 | ); 69 | } else if (error instanceof MCPToolError) { 70 | console.error( 71 | console.error( 72 | 'Tool error: ' + 73 | error.message + 74 | (error.toolName ? ` (Tool: ${error.toolName})` : '') + 75 | '\nPlease check if the tool exists and is properly configured.' 76 | ) 77 | ); 78 | } else if (error instanceof MCPConfigError) { 79 | console.error( 80 | console.error( 81 | 'Configuration error: ' + error.message + '\nPlease check your MCP configuration.' 82 | ) 83 | ); 84 | } else if (error instanceof Error) { 85 | console.error(console.error('Error: ' + error.message)); 86 | if (debug) { 87 | console.error(error.stack); 88 | } 89 | } else { 90 | console.error(console.error('An unknown error occurred')); 91 | } 92 | } 93 | } 94 | 95 | // Export the command instance 96 | export const mcp = new MCPCommand(); 97 | -------------------------------------------------------------------------------- /src/commands/mcp/search.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandGenerator, CommandOptions } from '../../types'; 2 | import { MarketplaceManager } from './marketplace.js'; 3 | 4 | export class SearchCommand implements Command { 5 | constructor(private marketplaceManager: MarketplaceManager) {} 6 | 7 | async *execute(query: string, options: CommandOptions): CommandGenerator { 8 | if (!query?.trim()) { 9 | throw new Error('Search query cannot be empty'); 10 | } 11 | 12 | yield `Searching for MCP servers and GitHub repositories matching: "${query}"\n`; 13 | yield `Processing query for intelligent search...\n`; 14 | 15 | const servers = await this.marketplaceManager.searchServers(query, options); 16 | if (servers.length === 0) { 17 | yield 'No results found matching your query.\n'; 18 | return; 19 | } 20 | 21 | if (options?.json) { 22 | yield JSON.stringify(servers, null, 2) + '\n'; 23 | return; 24 | } 25 | 26 | // Separate marketplace and GitHub results for display purposes 27 | const marketplaceServers = servers.filter((server) => !server.mcpId.startsWith('github-')); 28 | const githubRepos = servers.filter((server) => server.mcpId.startsWith('github-')); 29 | 30 | yield `\nFound ${servers.length} total results matching your query:\n`; 31 | 32 | // Display marketplace servers 33 | if (marketplaceServers.length > 0) { 34 | yield `\n${marketplaceServers.length} matching MCP marketplace servers:\n`; 35 | yield `${'='.repeat(40)}\n`; 36 | 37 | for (const server of marketplaceServers) { 38 | yield `\n${server.name}\n`; 39 | yield `Description: ${server.description}\n`; 40 | yield `Category: ${server.category}\n`; 41 | yield `Tags: ${server.tags.join(', ')}\n`; 42 | yield `GitHub: ${server.githubUrl}\n`; 43 | yield `${'─'.repeat(30)}\n`; 44 | } 45 | } 46 | 47 | // Display GitHub repositories 48 | if (githubRepos.length > 0) { 49 | yield `\n${githubRepos.length} matching GitHub repositories with MCP capabilities:\n`; 50 | yield `${'='.repeat(40)}\n`; 51 | 52 | for (const repo of githubRepos) { 53 | yield `\n${repo.name}\n`; 54 | yield `Description: ${repo.description}\n`; 55 | yield `Author: ${repo.author}\n`; 56 | yield `Stars: ${repo.githubStars}\n`; 57 | yield `GitHub: ${repo.githubUrl}\n`; 58 | yield `${'─'.repeat(30)}\n`; 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/test/FILESYSTEM_MCP_PLAN.md: -------------------------------------------------------------------------------- 1 | # Implementation Plan: Adding Filesystem MCP Server to Test Executor 2 | 3 | ## Overview 4 | 5 | This plan outlines the steps to add the filesystem MCP server to `src/commands/test/executor-new.ts` as a tool available to tests. The filesystem MCP will be configured with `@modelcontextprotocol/server-filesystem` and will only have access to the temporary test directory. We'll also update the prompt to inform the test executor about these limitations. 6 | 7 | ## Implementation Steps 8 | 9 | ### 1. Configure the Filesystem MCP Server 10 | 11 | We need to set up the filesystem MCP server with the following configuration: 12 | 13 | ```json 14 | "filesystem": { 15 | "command": "npx", 16 | "args": [ 17 | "-y", 18 | "@modelcontextprotocol/server-filesystem", 19 | "/path/to/temporary/test/directory" 20 | ] 21 | } 22 | ``` 23 | 24 | Where `/path/to/temporary/test/directory` will be the temporary directory created for each test scenario. 25 | 26 | This should be passed in to the UnifiedLLMClient constructor so that it has access to the tool 27 | 28 | ### 2. Update the System Prompt 29 | 30 | Update the system prompt to inform the test executor about the filesystem MCP tool and its limitations. The prompt should clearly explain: 31 | 32 | 1. That the filesystem MCP tool is available for file operations 33 | 2. The exact capabilities of the tool (read, write, list, etc.) 34 | 3. That it can ONLY access files within the temporary test directory 35 | 4. Examples of how to use the tool for common operations 36 | 37 | ```typescript 38 | const systemPrompt = `You are a testing agent for vibe-tools commands. Your task is to execute the test scenario provided using the tools available to determine if vibe-tools is working correctly and report the results. 39 | 40 | 41 | A filesystem MCP tool (filesystem_mcp) is available for file operations. This tool is configured via '@modelcontextprotocol/server-filesystem' and can ONLY access the temporary test directory (${tempDir}). 42 | 43 | The filesystem_mcp tool supports the following operations: 44 | - read: Read the contents of a file 45 | - write: Write content to a file 46 | - list: List files in a directory 47 | - exists: Check if a file or directory exists 48 | - mkdir: Create a directory 49 | 50 | Examples: 51 | - To read a file: filesystem_mcp({ operation: 'read', path: 'example.txt' }) 52 | - To write to a file: filesystem_mcp({ operation: 'write', path: 'example.txt', content: 'Hello world' }) 53 | - To list files: filesystem_mcp({ operation: 'list', path: '.' }) 54 | 55 | IMPORTANT: Do NOT attempt to use this tool to access files outside the temporary test directory (${tempDir}). 56 | 57 | 58 | ${CURSOR_RULES_TEMPLATE} 59 | 60 | Execute the test scenario provided and report the results. If you run into problems executing the scenario, make 3 attempts to execute the scenario. If you still run into problems after 3 attempts, report the results as FAIL. 61 | 62 | 63 | The available command line tools are vibe-tools, ${tools.map((t) => t.name).join(', ')}. Other command line tools are not permitted. 64 | Reply with your workings and your findings. Only use tools to perform the test; do not use tools to communicate your results. 65 | 66 | 67 | Update me on what you are doing as you execute the test scenario. Once you determine that it's passed or failed, report the results in the following format: 68 | ${jsonResponseInstructions} 69 | `; 70 | ``` 71 | -------------------------------------------------------------------------------- /src/commands/test/index.ts: -------------------------------------------------------------------------------- 1 | import { TestCommand } from './command'; 2 | 3 | export default TestCommand; 4 | -------------------------------------------------------------------------------- /src/commands/test/tools/command-execution.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ToolDefinition, 3 | ToolExecutionResult, 4 | } from '../../../utils/tool-enabled-llm/unified-client'; 5 | 6 | import * as childProcess from 'child_process'; 7 | import { promisify } from 'util'; 8 | 9 | const exec = promisify(childProcess.exec); 10 | 11 | /** 12 | * Create a tool definition for executing vibe-tools commands in the development environment 13 | * 14 | * @returns A tool definition for command execution 15 | */ 16 | export function createCommandExecutionTool(options: { debug: boolean }): ToolDefinition { 17 | return { 18 | name: 'execute_command', 19 | description: 'Execute a vibe-tools command in the development environment', 20 | parameters: { 21 | type: 'object', 22 | properties: { 23 | command: { 24 | type: 'string', 25 | description: 'The vibe-tools command to execute', 26 | }, 27 | }, 28 | required: ['command'], 29 | }, 30 | execute: async (args: { command: string }): Promise => { 31 | try { 32 | // Replace vibe-tools with pnpm dev to use development code 33 | const devCommand = args.command.replace(/^vibe-tools\s/, 'pnpm dev '); 34 | 35 | if (options.debug) { 36 | console.log(`\nExecuting command: ${devCommand}`); 37 | } 38 | 39 | let { stdout, stderr } = await exec(devCommand, { 40 | maxBuffer: 10 * 1024 * 1024, // 10MB buffer to handle large outputs 41 | }); 42 | 43 | stdout = stdout.trim(); 44 | stderr = stderr.trim(); 45 | 46 | if (stderr && !stdout) { 47 | return { 48 | success: false, 49 | output: stderr, 50 | error: { 51 | message: 'Command returned error output', 52 | details: { stderr }, 53 | }, 54 | }; 55 | } 56 | 57 | // Return successful result with both stdout and stderr if available 58 | return { 59 | success: true, 60 | output: stdout || 'Command executed successfully with no output', 61 | ...(stderr 62 | ? { error: { message: 'Warning: stderr output was present', details: { stderr } } } 63 | : {}), 64 | }; 65 | } catch (error) { 66 | // Handle specific error types with detailed information 67 | if (error instanceof Error) { 68 | const errorObj: ToolExecutionResult & { 69 | error: { message: string; code?: number; details?: Record }; 70 | } = { 71 | success: false, 72 | output: `Command execution failed: ${error.message}`, 73 | error: { 74 | message: error.message, 75 | }, 76 | }; 77 | 78 | // Extract exit code from child process error if available 79 | if ('code' in error && typeof error.code === 'number') { 80 | errorObj.error.code = error.code; 81 | } 82 | 83 | // For command not found or permission errors 84 | if (error.message.includes('command not found') || error.message.includes('ENOENT')) { 85 | errorObj.error.details = { 86 | type: 'COMMAND_NOT_FOUND', 87 | suggestion: 'Ensure the command is installed and in your PATH', 88 | }; 89 | } else if ( 90 | error.message.includes('permission denied') || 91 | error.message.includes('EACCES') 92 | ) { 93 | errorObj.error.details = { 94 | type: 'PERMISSION_DENIED', 95 | suggestion: 'Check file permissions or try with appropriate privileges', 96 | }; 97 | } 98 | 99 | return errorObj; 100 | } 101 | 102 | return { 103 | success: false, 104 | output: 'Command execution failed with unknown error', 105 | error: { 106 | message: 'Unknown error', 107 | }, 108 | }; 109 | } 110 | }, 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /src/commands/test/types.ts: -------------------------------------------------------------------------------- 1 | import type { CommandOptions, Provider } from '../../types'; 2 | import type { ToolExecutionResult } from '../../utils/tool-enabled-llm/unified-client'; 3 | import { AssetReference } from '../../utils/assets'; 4 | import { z } from 'zod'; 5 | 6 | /** 7 | * A specialized AsyncGenerator for test commands that can return TestReport 8 | */ 9 | export type TestCommandGenerator = AsyncGenerator; 10 | 11 | /** 12 | * Extended command options for the test command 13 | */ 14 | export interface TestOptions extends CommandOptions { 15 | output?: string; 16 | parallel?: number; 17 | branch?: string; 18 | compareWith?: string; 19 | timeout?: number; 20 | retries?: number; 21 | tag?: string; 22 | mcpServers?: string[]; // Optional MCP servers to include in testing 23 | scenarios?: string; // Comma-separated list of scenario numbers to run 24 | provider?: Provider; 25 | /** 26 | * Maximum number of files to process concurrently. 27 | * @defaultValue 3 28 | */ 29 | fileConcurrency?: number; 30 | /** 31 | * Skip intermediate output during test execution. 32 | * @defaultValue false 33 | */ 34 | skipIntermediateOutput?: boolean; 35 | } 36 | 37 | /** 38 | * Represents a test scenario in a feature behavior file 39 | */ 40 | export interface TestScenario { 41 | id: string; 42 | type: string; 43 | description: string; 44 | taskDescription: string; 45 | expectedBehavior: string[]; 46 | successCriteria: string[]; 47 | tags?: string[]; 48 | assets?: Record; 49 | } 50 | 51 | /** 52 | * Represents the parsed content of a feature behavior file 53 | */ 54 | export interface FeatureBehavior { 55 | name: string; 56 | description: string; 57 | scenarios: TestScenario[]; 58 | } 59 | 60 | /** 61 | * Represents the result of executing a test scenario 62 | */ 63 | export interface TestScenarioResult { 64 | id: string; 65 | type: string; 66 | description: string; 67 | taskDescription: string; 68 | approachTaken: string; 69 | commands: string[]; 70 | actualCommands?: string[]; // The actual commands executed with pnpm dev prefix 71 | output: string; 72 | outputBuffer?: string[]; // Buffer to store all output from this scenario 73 | toolExecutions?: Array<{ tool: string; args: any; result: ToolExecutionResult }>; // History of tool executions 74 | expectedBehavior: { 75 | behavior: string; 76 | met: boolean; 77 | explanation?: string; // Explanation of why the behavior was met or not 78 | }[]; 79 | successCriteria: { 80 | criteria: string; 81 | met: boolean; 82 | explanation?: string; // Explanation of why the criteria was met or not 83 | }[]; 84 | result: 'PASS' | 'FAIL'; 85 | executionTime: number; 86 | attempts?: number; // Number of attempts made to execute the scenario 87 | explanation?: string; // Overall explanation of the test result 88 | error?: string; 89 | } 90 | 91 | /** 92 | * Represents a complete test report for a feature behavior file 93 | */ 94 | export interface TestReport { 95 | featureName: string; 96 | description: string; 97 | scenarios: TestScenarioResult[]; 98 | timestamp: string; 99 | branch: string; 100 | provider: string; 101 | model: string; 102 | os: string; 103 | nodeVersion: string; 104 | overallResult: 'PASS' | 'FAIL'; 105 | failedScenarios: string[]; 106 | passedScenarios?: number; 107 | totalExecutionTime: number; 108 | } 109 | 110 | /** 111 | * Exponential backoff retry configuration 112 | */ 113 | export interface RetryConfig { 114 | initialDelay: number; 115 | maxDelay: number; 116 | factor: number; 117 | retries: number; 118 | jitter: boolean; 119 | } 120 | 121 | // New Zod schema for the simplified test result JSON response 122 | export const TestResultSchema = z.object({ 123 | id: z.string(), 124 | status: z.string().transform((val) => { 125 | const normalized = val.trim().toUpperCase(); 126 | return normalized === 'PASS' ? 'PASS' : 'FAIL'; 127 | }), 128 | summary: z.string(), 129 | executionTime: z.number(), 130 | error: z.string().nullable().optional().default(null), 131 | }); 132 | 133 | export type TestResult = z.infer; 134 | -------------------------------------------------------------------------------- /src/commands/test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { promisify } from 'util'; 3 | import { exec as execCallback } from 'child_process'; 4 | import * as fs from 'fs'; 5 | import { mkdir } from 'fs/promises'; 6 | import { RetryConfig } from './types'; 7 | import fastGlob from 'fast-glob'; 8 | 9 | const exec = promisify(execCallback); 10 | export const readFile = promisify(fs.readFile); 11 | 12 | /** 13 | * Get the current git branch 14 | * 15 | * @returns The current branch name or 'unknown-branch' if not in a git repository 16 | */ 17 | export async function getCurrentBranch(): Promise { 18 | try { 19 | const { stdout } = await exec('git branch --show-current'); 20 | return stdout.trim() || 'unknown-branch'; 21 | } catch (error) { 22 | console.error('Error determining current branch:', error); 23 | return 'unknown-branch'; 24 | } 25 | } 26 | 27 | /** 28 | * Create a directory if it doesn't exist 29 | * 30 | * @param dir - Directory path to create 31 | */ 32 | export async function createDirIfNotExists(dir: string): Promise { 33 | await mkdir(dir, { recursive: true }); 34 | } 35 | 36 | /** 37 | * Find all feature behavior files matching a pattern 38 | * 39 | * @param pattern - Glob pattern to match feature behavior files 40 | * @returns AsyncGenerator that yields file paths as they are found 41 | */ 42 | export async function* findFeatureBehaviorFiles( 43 | pattern: string 44 | ): AsyncGenerator { 45 | const stream = fastGlob.stream(pattern, { 46 | absolute: false, // Return paths relative to cwd 47 | dot: true, // Include dotfiles 48 | followSymbolicLinks: true, // Follow symlinks 49 | onlyFiles: true, // Only return files, not directories 50 | }); 51 | 52 | for await (const file of stream) { 53 | yield file.toString(); 54 | } 55 | } 56 | 57 | /** 58 | * Find all feature behavior files matching a pattern (non-streaming version) 59 | * 60 | * @param pattern - Glob pattern to match feature behavior files 61 | * @returns Promise resolving to array of file paths 62 | * @deprecated Use the streaming version instead 63 | */ 64 | export async function findFeatureBehaviorFilesArray(pattern: string): Promise { 65 | return fastGlob(pattern); 66 | } 67 | 68 | /** 69 | * Generate a standardized report filename for a feature behavior file 70 | * 71 | * @param featureBehaviorFile - Path to the feature behavior file 72 | * @returns Report filename 73 | */ 74 | export function getReportFilename(featureBehaviorFile: string): string { 75 | const baseName = path.basename(featureBehaviorFile, '.md'); 76 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); 77 | return `${baseName}_report_${timestamp}.md`; 78 | } 79 | 80 | /** 81 | * Generate a standardized result filename for a feature behavior file 82 | * 83 | * @param featureBehaviorFile - Path to the feature behavior file 84 | * @returns Result filename 85 | */ 86 | export function getResultFilename(featureBehaviorFile: string): string { 87 | const baseName = path.basename(featureBehaviorFile, '.md'); 88 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); 89 | return `${baseName}_result_${timestamp}.txt`; 90 | } 91 | 92 | /** 93 | * Check if an error is transient (network error, rate limit, etc.) 94 | * 95 | * @param error - The error to check 96 | * @returns True if the error is transient and should be retried 97 | */ 98 | export function isTransientError(error: unknown): boolean { 99 | // Network errors 100 | if (error instanceof Error) { 101 | const message = error.message.toLowerCase(); 102 | return ( 103 | message.includes('network') || 104 | message.includes('timeout') || 105 | message.includes('rate limit') || 106 | message.includes('throttle') || 107 | message.includes('eai_again') || 108 | message.includes('socket') || 109 | message.includes('connection') || 110 | message.includes('temporary') 111 | ); 112 | } 113 | return false; 114 | } 115 | 116 | /** 117 | * Calculate exponential backoff delay with jitter 118 | * 119 | * @param attempt - Current attempt number (1-based) 120 | * @param config - Retry configuration 121 | * @returns Delay in milliseconds 122 | */ 123 | export function calculateBackoffDelay(attempt: number, config: RetryConfig): number { 124 | const { initialDelay, maxDelay, factor, jitter } = config; 125 | 126 | // Calculate base delay with exponential factor 127 | let delay = initialDelay * Math.pow(factor, attempt - 1); 128 | 129 | // Apply jitter 130 | if (jitter) { 131 | // Add random jitter between -25% and +25% 132 | const jitterFactor = 0.25; 133 | const randomJitter = 1 - jitterFactor + Math.random() * jitterFactor * 2; 134 | delay = delay * randomJitter; 135 | } 136 | 137 | // Cap at maximum delay 138 | return Math.min(delay, maxDelay); 139 | } 140 | 141 | /** 142 | * Sleep for a specified duration 143 | * 144 | * @param ms - Duration in milliseconds 145 | */ 146 | export function sleep(ms: number): Promise { 147 | return new Promise((resolve) => setTimeout(resolve, ms)); 148 | } 149 | -------------------------------------------------------------------------------- /src/commands/wait.ts: -------------------------------------------------------------------------------- 1 | import type { Command, CommandGenerator, CommandOptions } from '../types'; 2 | 3 | export class WaitCommand implements Command { 4 | async *execute(query: string, options: CommandOptions): CommandGenerator { 5 | const seconds = parseInt(query, 10); 6 | if (isNaN(seconds) || seconds <= 0) { 7 | yield 'Error: Please provide a positive number of seconds to wait.'; 8 | return; 9 | } 10 | 11 | yield `Waiting for ${seconds} second(s)...`; 12 | await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); 13 | yield `Finished waiting for ${seconds} second(s).`; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/xcode/command.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the Xcode command functionality in vibe-tools. 3 | * This file exports the main XcodeCommand instance that handles all Xcode-related operations. 4 | * 5 | * The command is designed to be simple and focused, delegating actual functionality 6 | * to subcommands like 'build' for better organization and maintainability. 7 | */ 8 | 9 | import { XcodeCommand } from './xcode.js'; 10 | 11 | // Export a singleton instance of XcodeCommand to be used by the CLI 12 | export default new XcodeCommand(); 13 | -------------------------------------------------------------------------------- /src/commands/xcode/lint.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Implementation of the Xcode lint command. 3 | * This command analyzes code and offers to fix warnings. 4 | * 5 | * Key features: 6 | * - Warning analysis 7 | * - Interactive fixes 8 | * - SwiftLint integration 9 | */ 10 | 11 | import type { Command, CommandGenerator, CommandOptions } from '../../types'; 12 | import { findXcodeProject } from './utils.js'; 13 | import { BuildCommand } from './build.js'; 14 | 15 | export class LintCommand implements Command { 16 | /** 17 | * Main execution method for the lint command. 18 | * Analyzes code and offers to fix warnings. 19 | * 20 | * @param query - Command query string (unused) 21 | * @param options - Command options 22 | * @yields Status messages and command output 23 | */ 24 | async *execute(query: string, options: CommandOptions): CommandGenerator { 25 | try { 26 | const dir = process.cwd(); 27 | const project = findXcodeProject(dir); 28 | if (!project) { 29 | throw new Error('No Xcode project or workspace found in current directory'); 30 | } 31 | 32 | yield 'Building project to check for warnings...\n'; 33 | 34 | // First build the project to get warnings 35 | const buildCommand = new BuildCommand(); 36 | let warnings: string[] = []; 37 | try { 38 | yield* buildCommand.execute('', options); 39 | } catch (error: any) { 40 | // Capture any build output that might contain warnings 41 | if (error.message) { 42 | warnings = error.message 43 | .split('\n') 44 | .filter((line: string) => line.includes(': warning:')) 45 | .map((line: string) => line.trim()); 46 | } 47 | if (error.message.includes('Build failed due to errors')) { 48 | throw new Error( 49 | 'Cannot analyze warnings while there are build errors. Please fix errors first.' 50 | ); 51 | } 52 | } 53 | 54 | if (warnings.length === 0) { 55 | yield 'No warnings found!\n'; 56 | return; 57 | } 58 | 59 | yield `\nFound ${warnings.length} warnings to analyze:\n`; 60 | for (const warning of warnings) { 61 | yield `${warning}\n`; 62 | } 63 | 64 | yield '\nTo fix these warnings:\n'; 65 | yield '1. Use SwiftLint to enforce consistent style:\n'; 66 | yield ' brew install swiftlint\n'; 67 | yield ' Add .swiftlint.yml to your project\n'; 68 | yield '\n2. Common fixes:\n'; 69 | yield ' - Unused variables: Remove or use `_`\n'; 70 | yield ' - Long lines: Break into multiple lines\n'; 71 | yield ' - Force unwrap: Use optional binding\n'; 72 | yield ' - Implicit type: Explicitly declare types\n'; 73 | yield '\n3. Run SwiftLint auto-correct:\n'; 74 | yield ' swiftlint --fix\n'; 75 | } catch (error: any) { 76 | console.error(`Lint failed: ${error}`); 77 | throw error; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/commands/xcode/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared utilities for Xcode commands. 3 | * Contains common functionality used across different Xcode command implementations. 4 | */ 5 | 6 | import { readdirSync } from 'node:fs'; 7 | 8 | /** 9 | * Information about an Xcode project 10 | */ 11 | export interface XcodeProject { 12 | path: string; 13 | type: 'project' | 'workspace'; 14 | name: string; 15 | } 16 | 17 | /** 18 | * Represents a build error or warning from Xcode 19 | */ 20 | export interface XcodeBuildError { 21 | file?: string; // Source file where the error occurred 22 | line?: number; // Line number in the file 23 | column?: number; // Column number in the file 24 | message: string; // Error message 25 | type: 'error' | 'warning' | 'note'; // Severity of the issue 26 | } 27 | 28 | /** 29 | * Maps device types to their simulator names. 30 | * These are the latest device types available in Xcode 15. 31 | */ 32 | export const DEVICE_TYPES = { 33 | iphone: 'iPhone 15 Pro Max', 34 | ipad: 'iPad Pro 13-inch (M4)', 35 | } as const; 36 | 37 | /** 38 | * Default timeouts for various operations 39 | */ 40 | export const DEFAULT_TIMEOUTS = { 41 | SIMULATOR_BOOT: 5000, 42 | APP_LAUNCH: 3000, 43 | }; 44 | 45 | /** 46 | * Finds an Xcode project or workspace in the given directory. 47 | * Prefers workspaces over projects as they're more common in modern apps. 48 | * 49 | * @param dir - Directory to search in 50 | * @returns Project info or null if none found 51 | */ 52 | export function findXcodeProject(dir: string): XcodeProject | null { 53 | const files = readdirSync(dir); 54 | 55 | // First check for workspace since that's more common in modern projects 56 | const workspace = files.find((f) => f.endsWith('.xcworkspace')); 57 | if (workspace) { 58 | return { 59 | path: workspace, 60 | type: 'workspace', 61 | name: workspace.replace('.xcworkspace', ''), 62 | }; 63 | } 64 | 65 | // Then check for project 66 | const project = files.find((f) => f.endsWith('.xcodeproj')); 67 | if (project) { 68 | return { 69 | path: project, 70 | type: 'project', 71 | name: project.replace('.xcodeproj', ''), 72 | }; 73 | } 74 | 75 | return null; 76 | } 77 | -------------------------------------------------------------------------------- /src/commands/xcode/xcode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Main orchestrator for Xcode-related commands in vibe-tools. 3 | * Implements the Command interface and manages subcommands for different Xcode operations. 4 | * 5 | * Currently supported subcommands: 6 | * - build: Builds the Xcode project and reports errors 7 | * - lint: Analyzes code and offers to fix warnings 8 | * - run: Builds and runs the app in simulator 9 | * 10 | * The command uses a subcommand pattern to keep the codebase organized and extensible. 11 | * Each subcommand is implemented as a separate class that handles its specific functionality. 12 | */ 13 | 14 | import type { Command, CommandGenerator, CommandOptions } from '../../types'; 15 | import { BuildCommand } from './build.js'; 16 | import { RunCommand } from './run.js'; 17 | import { LintCommand } from './lint.js'; 18 | 19 | /** 20 | * Maps subcommand names to their implementing classes. 21 | * This allows easy addition of new subcommands without modifying existing code. 22 | */ 23 | type SubcommandMap = { 24 | [key: string]: Command; 25 | }; 26 | 27 | export class XcodeCommand implements Command { 28 | /** 29 | * Registry of available subcommands. 30 | * Each subcommand is instantiated once and reused for all invocations. 31 | */ 32 | private subcommands: SubcommandMap = { 33 | build: new BuildCommand(), 34 | run: new RunCommand(), 35 | lint: new LintCommand(), 36 | }; 37 | 38 | /** 39 | * Main execution method for the Xcode command. 40 | * Parses the input query to determine which subcommand to run. 41 | * 42 | * @param query - The command query string (e.g., "build" or "run iphone") 43 | * @param options - Global command options that apply to all subcommands 44 | * @yields Status messages and command output 45 | */ 46 | async *execute(query: string, options: CommandOptions): CommandGenerator { 47 | // Split query into subcommand and remaining args 48 | const [subcommand, ...args] = query.split(' '); 49 | 50 | // If no subcommand provided, show help 51 | if (!subcommand) { 52 | yield 'Usage: vibe-tools xcode [args]\n'; 53 | yield 'Available subcommands:'; 54 | yield ' build Build Xcode project and report errors'; 55 | yield ' lint Analyze code and offer to fix warnings'; 56 | yield ' run Build and run on simulator (iphone/ipad)\n'; 57 | yield 'Examples:'; 58 | yield ' vibe-tools xcode build'; 59 | yield ' vibe-tools xcode lint'; 60 | yield ' vibe-tools xcode run iphone'; 61 | yield ' vibe-tools xcode run ipad'; 62 | return; 63 | } 64 | 65 | // Get the subcommand handler 66 | const handler = this.subcommands[subcommand]; 67 | if (!handler) { 68 | yield `Unknown subcommand: ${subcommand}\n`; 69 | yield 'Available subcommands:'; 70 | yield ' build Build Xcode project and report errors'; 71 | yield ' lint Analyze code and offer to fix warnings'; 72 | yield ' run Build and run on simulator (iphone/ipad)'; 73 | return; 74 | } 75 | 76 | // Execute the subcommand with remaining args 77 | yield* handler.execute(args.join(' '), options); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/commands/youtube/index.ts: -------------------------------------------------------------------------------- 1 | import { YouTubeCommand } from './youtube'; 2 | 3 | export default YouTubeCommand; 4 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from './types'; 2 | 3 | // 8000 is the max tokens for the perplexity models 4 | // most openai and anthropic models are 8192 5 | // so we just use 8000 for all the defaults so people have a fighting chance of not hitting the limits 6 | export const defaultMaxTokens = 8000; 7 | export const defaultConfig: Config = { 8 | ide: 'cursor', // Default IDE 9 | web: { 10 | provider: 'perplexity', 11 | }, 12 | plan: { 13 | fileProvider: 'gemini', 14 | thinkingProvider: 'openai', 15 | fileMaxTokens: defaultMaxTokens, 16 | thinkingMaxTokens: defaultMaxTokens, 17 | }, 18 | repo: { 19 | provider: 'gemini', 20 | maxTokens: defaultMaxTokens, 21 | }, 22 | doc: { 23 | maxRepoSizeMB: 100, 24 | provider: 'perplexity', 25 | maxTokens: defaultMaxTokens, 26 | }, 27 | browser: { 28 | headless: true, 29 | defaultViewport: '1280x720', 30 | timeout: 120000, 31 | }, 32 | stagehand: { 33 | provider: 'anthropic', 34 | verbose: false, 35 | debugDom: false, 36 | enableCaching: true, 37 | }, 38 | tokenCount: { 39 | encoding: 'o200k_base', 40 | }, 41 | perplexity: { 42 | model: 'sonar-pro', 43 | maxTokens: defaultMaxTokens, 44 | }, 45 | reasoningEffort: 'medium', // Default reasoning effort for all commands 46 | 47 | // Note that it is also permitted to add provider-specific config options 48 | // in the config file, even though they are not shown in this interface. 49 | // command specific configuration always overrides the provider specific 50 | // configuration 51 | // modelbox: { 52 | // model: 'google/gemini-2.5-flash-preview-05-20', // Default model, can be overridden per command 53 | // maxTokens: 8192, 54 | // }, 55 | // openrouter: { 56 | // model: 'google/gemini-2.5-pro-preview' 57 | // } 58 | // 59 | // or 60 | // 61 | // "gemini": { 62 | // "model": "gemini-2.5-pro-preview", 63 | // "maxTokens": 10000 64 | // } 65 | // 66 | // or 67 | // 68 | // "openai": { 69 | // "model": "gpt-4o", 70 | // "maxTokens": 10000 71 | // } 72 | // 73 | // these would apply if the command was run with the --provider flag 74 | // or if provider is configured for a command without additional fields 75 | // e.g. 76 | // 77 | // "repo": { 78 | // "provider": "openai", 79 | // } 80 | // 81 | // "docs": { 82 | // "provider": "gemini", 83 | // } 84 | // 85 | // You can also configure MCP server overrides: 86 | // 87 | // "mcp": { 88 | // "overrides": { 89 | // "my-server": { 90 | // "githubUrl": "https://github.com/myuser/my-server", 91 | // "command": "npx", 92 | // "args": ["-y", "github:myuser/my-server@main"] 93 | // } 94 | // } 95 | // } 96 | }; 97 | 98 | import { existsSync, readFileSync } from 'node:fs'; 99 | import { join } from 'node:path'; 100 | import { homedir } from 'node:os'; 101 | import dotenv from 'dotenv'; 102 | import { once } from './utils/once'; 103 | 104 | export function loadConfig(): Config { 105 | // Try loading from current directory first 106 | try { 107 | const localConfigPath = join(process.cwd(), 'vibe-tools.config.json'); 108 | const localConfig = JSON.parse(readFileSync(localConfigPath, 'utf-8')); 109 | return { ...defaultConfig, ...localConfig }; 110 | } catch { 111 | // If local config doesn't exist, try home directory 112 | try { 113 | const homeConfigPath = join(homedir(), '.vibe-tools', 'config.json'); 114 | const homeConfig = JSON.parse(readFileSync(homeConfigPath, 'utf-8')); 115 | return { ...defaultConfig, ...homeConfig }; 116 | } catch { 117 | // If neither config exists, return default config 118 | return defaultConfig; 119 | } 120 | } 121 | } 122 | 123 | export function applyEnvUnset(): void { 124 | // Check for CURSOR_TOOLS_ENV_UNSET environment variable 125 | const envUnset = process.env.CURSOR_TOOLS_ENV_UNSET; 126 | if (envUnset) { 127 | // Parse comma-separated list of keys to unset 128 | const keysToUnset = envUnset.split(',').map((key) => key.trim()); 129 | if (keysToUnset.length > 0) { 130 | console.log(`Unsetting environment variables: ${keysToUnset.join(', ')}`); 131 | // Unset each key 132 | for (const key of keysToUnset) { 133 | delete process.env[key]; 134 | } 135 | } 136 | } 137 | } 138 | 139 | function _loadEnv(): void { 140 | // Try loading from current directory first 141 | const localEnvPath = join(process.cwd(), '.vibe-tools.env'); 142 | if (existsSync(localEnvPath)) { 143 | console.log('Local .env file found, loading env from', localEnvPath); 144 | dotenv.config({ path: localEnvPath }); 145 | return; 146 | } 147 | 148 | // If local env doesn't exist, try home directory 149 | const homeEnvPath = join(homedir(), '.vibe-tools', '.env'); 150 | if (existsSync(homeEnvPath)) { 151 | console.log('Home .env file found, loading env from', homeEnvPath); 152 | dotenv.config({ path: homeEnvPath }); 153 | return; 154 | } 155 | 156 | // If neither env file exists, continue without loading 157 | console.log('No .env file found in local or home directories.', localEnvPath, homeEnvPath); 158 | return; 159 | } 160 | 161 | export const loadEnv = once(() => { 162 | _loadEnv(); 163 | applyEnvUnset(); 164 | }); 165 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | // Base error class for all vibe-tools errors 2 | export class CursorToolsError extends Error { 3 | constructor( 4 | message: string, 5 | public readonly details?: unknown 6 | ) { 7 | super(message); 8 | this.cause = details; 9 | this.name = 'CursorToolsError'; 10 | } 11 | 12 | // Format error message for user display 13 | public formatUserMessage(debug = false): string { 14 | let message = `${this.message}`; 15 | 16 | if (debug && this.details) { 17 | message += `\nDetails: ${JSON.stringify(this.details, null, 2)}`; 18 | } 19 | 20 | return message; 21 | } 22 | } 23 | 24 | // Provider-related errors 25 | export class ProviderError extends CursorToolsError { 26 | constructor(message: string, details?: unknown) { 27 | super(message); 28 | this.name = 'ProviderError'; 29 | if (details instanceof Error) { 30 | this.cause = details; 31 | } else if (details) { 32 | console.error(message, details); 33 | } 34 | } 35 | } 36 | 37 | export class ApiKeyMissingError extends ProviderError { 38 | constructor(provider: string) { 39 | super( 40 | `API key for ${provider} is not set. Please set the ${provider.toUpperCase()}_API_KEY environment variable in your .vibe-tools.env file located in your home directory (~/.vibe-tools/.env). 41 | 42 | For more information on setting up API keys, visit: https://github.com/cursor-ai/vibe-tools#api-keys`, 43 | { provider } 44 | ); 45 | this.name = 'ApiKeyMissingError'; 46 | } 47 | } 48 | 49 | export class ModelNotFoundError extends ProviderError { 50 | constructor(provider: string) { 51 | let message = `No model specified for ${provider}.`; 52 | 53 | // Add model suggestions based on provider 54 | switch (provider) { 55 | case 'openai': 56 | message += '\nSuggested models:\n- gpt-4o\n- o3-mini'; 57 | break; 58 | case 'anthropic': 59 | message += '\nSuggested models:\n- claude-3-5-opus-latest\n- claude-sonnet-4-20250514'; 60 | break; 61 | case 'gemini': 62 | message += 63 | '\nSuggested models:\n- gemini-2.5-flash-preview-05-20\n- gemini-2.5-pro-preview\n- gemini-2.5-pro-preview'; 64 | break; 65 | case 'perplexity': 66 | message += '\nSuggested models:\n- sonar-pro\n- sonar-reasoning-pro'; 67 | break; 68 | case 'openrouter': 69 | message += 70 | '\nSuggested models:\n- perplexity/sonar\n- openai/gpt-4o\n- anthropic/claude-sonnet-4\n- deepseek/deepseek-r1:free\n- google/gemini-2.5-pro-preview\n- mistral/mistral-large\n- groq/llama2-70b'; 71 | break; 72 | case 'modelbox': 73 | message += 74 | '\nSuggested models:\n- perplexity/sonar-pro\n- openai/gpt-4o\n- anthropic/claude-sonnet-4'; 75 | break; 76 | case 'xai': 77 | message += '\nSuggested models:\n- grok-3-latest\n- grok-3-mini-latest'; 78 | break; 79 | } 80 | 81 | message += '\nUse --model to specify a model.'; 82 | 83 | super(message, { provider, model: undefined }); 84 | this.name = 'ModelNotFoundError'; 85 | } 86 | } 87 | 88 | export class NetworkError extends ProviderError { 89 | constructor(message: string, details?: unknown) { 90 | super(`Network error: ${message}`, details); 91 | this.name = 'NetworkError'; 92 | } 93 | } 94 | 95 | export class GeminiRecitationError extends ProviderError { 96 | constructor(message?: string) { 97 | super( 98 | message || 99 | 'Gemini was unable to provide an original response and may be reciting the prompt. Please rephrase your query.', 100 | { finishReason: 'RECITATION' } 101 | ); 102 | this.name = 'GeminiRecitationError'; 103 | } 104 | } 105 | 106 | // File-related errors 107 | export class FileError extends CursorToolsError { 108 | constructor(message: string, details?: unknown) { 109 | super(message, details); 110 | this.name = 'FileError'; 111 | } 112 | } 113 | 114 | // Test-related errors 115 | export class TestError extends CursorToolsError { 116 | constructor(message: string, details?: unknown) { 117 | super(`Test error: ${message}`, details); 118 | this.name = 'TestError'; 119 | } 120 | } 121 | 122 | export class FeatureFileParseError extends TestError { 123 | constructor(filePath: string, details?: unknown) { 124 | super(`Failed to parse feature behavior file: ${filePath}`, details); 125 | this.name = 'FeatureFileParseError'; 126 | } 127 | } 128 | 129 | export class TestExecutionError extends TestError { 130 | constructor(message: string, details?: unknown) { 131 | super(message, details); 132 | this.name = 'TestExecutionError'; 133 | } 134 | } 135 | 136 | export class TestTimeoutError extends TestError { 137 | constructor(scenario: string, timeoutSeconds: number, details?: unknown) { 138 | super(`Test scenario '${scenario}' timed out after ${timeoutSeconds} seconds`, details); 139 | this.name = 'TestTimeoutError'; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/llms/index.ts: -------------------------------------------------------------------------------- 1 | import type { PackageRuleItem } from 'vibe-rules'; 2 | import { VIBE_TOOLS_CORE_CONTENT, VIBE_TOOLS_RULES_VERSION } from '../vibe-rules.js'; 3 | 4 | const processedRuleContent = VIBE_TOOLS_CORE_CONTENT.replace( 5 | '${VIBE_TOOLS_RULES_VERSION}', 6 | VIBE_TOOLS_RULES_VERSION 7 | ); 8 | 9 | const rules: PackageRuleItem[] = [ 10 | { 11 | name: 'vibe-tools-rule', 12 | description: 'Vibe Tools', 13 | rule: processedRuleContent, 14 | alwaysApply: true, 15 | globs: ['*', '**/*'], 16 | }, 17 | ]; 18 | 19 | export default rules; 20 | -------------------------------------------------------------------------------- /src/providers/notFoundErrors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility function to detect if an error is related to a model not being found 3 | * across different provider APIs. 4 | * 5 | * @param error - The error object from the API call 6 | * @returns boolean indicating if this is a model not found error 7 | */ 8 | export function isModelNotFoundError(error: unknown): boolean { 9 | // Check for error properties from structured errors 10 | if (error && typeof error === 'object') { 11 | // Check error codes (common in OpenAI, OpenRouter) 12 | if ( 13 | 'code' in error && 14 | typeof error.code === 'string' && 15 | ['model_not_found', 'invalid_model', 'model_not_available'].includes(error.code) 16 | ) { 17 | return true; 18 | } 19 | 20 | // Check error types (common in Anthropic) 21 | if ( 22 | 'type' in error && 23 | typeof error.type === 'string' && 24 | ['invalid_request_error', 'model_error'].includes(error.type) 25 | ) { 26 | // For these error types, we should check if the message relates to model not found 27 | if ('message' in error && typeof error.message === 'string') { 28 | return isModelNotFoundErrorMessage(error.message); 29 | } 30 | } 31 | 32 | // Check status code (common in Anthropic) 33 | if ('status' in error && error.status === 404) { 34 | return true; 35 | } 36 | } 37 | 38 | // Check error message patterns 39 | if (error instanceof Error) { 40 | return isModelNotFoundErrorMessage(error.message); 41 | } 42 | 43 | return false; 44 | } 45 | 46 | /** 47 | * Helper function to check if an error message indicates a model not found error 48 | * 49 | * @param message - The error message to check 50 | * @returns boolean indicating if this message suggests a model not found error 51 | */ 52 | function isModelNotFoundErrorMessage(message: string): boolean { 53 | const lowerMessage = message.toLowerCase(); 54 | 55 | // Common error message patterns across providers 56 | const modelNotFoundPatterns = [ 57 | 'model not found', 58 | 'no model', 59 | 'invalid model', 60 | 'does not exist', 61 | 'unavailable model', 62 | 'model is not supported', 63 | 'model invalid', 64 | 'model could not be found', 65 | 'the model', // This is too general by itself, needs to be combined 66 | ]; 67 | 68 | return ( 69 | modelNotFoundPatterns.some((pattern) => lowerMessage.includes(pattern)) && 70 | // If it includes "the model", ensure it's combined with another indicator 71 | (lowerMessage.includes('the model') 72 | ? lowerMessage.includes('does not exist') || 73 | lowerMessage.includes('unavailable') || 74 | lowerMessage.includes('invalid') || 75 | lowerMessage.includes('not found') 76 | : true) 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/types/browser/browser.ts: -------------------------------------------------------------------------------- 1 | import type { z } from 'zod'; 2 | 3 | // Core Stagehand API interface 4 | export interface Stagehand { 5 | act(action: string): Promise; 6 | extract(schema: z.ZodType): Promise; 7 | observe(): Promise; 8 | } 9 | 10 | // Configuration options 11 | export interface StagehandConfig { 12 | env: 'LOCAL' | 'BROWSERBASE'; 13 | headless: boolean; 14 | verbose: 0 | 1 | 2; 15 | debugDom: boolean; 16 | enableCaching: boolean; 17 | browserbaseApiKey?: string; 18 | browserbaseProjectId?: string; 19 | llmProvider?: LLMProvider; 20 | openaiApiKey?: string; 21 | anthropicApiKey?: string; 22 | googleApiKey?: string; 23 | } 24 | 25 | export type LLMProvider = 'openai' | 'anthropic' | 'google'; 26 | 27 | // Stagehand method options 28 | export interface ActOptions { 29 | instruction: string; 30 | timeout?: number; 31 | retries?: number; 32 | } 33 | 34 | export interface ExtractOptions { 35 | timeout?: number; 36 | retries?: number; 37 | } 38 | 39 | export interface ObserveOptions { 40 | timeout?: number; 41 | retries?: number; 42 | } 43 | 44 | // Observation result type 45 | export interface ObservationResult { 46 | elements: { 47 | type: string; 48 | description: string; 49 | actions: string[]; 50 | location: string; 51 | }[]; 52 | summary: string; 53 | } 54 | 55 | // Command options shared across all browser commands 56 | export interface BrowserCommandOptions { 57 | url: string; 58 | debug?: boolean; 59 | saveTo?: string; 60 | headless?: boolean; 61 | timeout?: number; 62 | viewport?: { 63 | width: number; 64 | height: number; 65 | }; 66 | } 67 | 68 | // Extract command specific options 69 | export interface ExtractCommandOptions extends BrowserCommandOptions { 70 | schema?: string | object; 71 | } 72 | 73 | // Act command specific options 74 | export interface ActCommandOptions extends BrowserCommandOptions { 75 | instruction: string; 76 | } 77 | 78 | // Observe command specific options 79 | export interface ObserveCommandOptions extends BrowserCommandOptions { 80 | instruction?: string; 81 | } 82 | -------------------------------------------------------------------------------- /src/utils/AsyncReturnType.ts: -------------------------------------------------------------------------------- 1 | export type AsyncReturnType Promise> = T extends ( 2 | ...args: any[] 3 | ) => Promise 4 | ? U 5 | : T; 6 | -------------------------------------------------------------------------------- /src/utils/assets.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { promisify } from 'util'; 4 | 5 | const readFile = promisify(fs.readFile); 6 | 7 | /** 8 | * Represents an asset reference in a test scenario 9 | */ 10 | export interface AssetReference { 11 | type: AssetType; 12 | name: string; 13 | content?: string; // For inline assets 14 | path?: string; // For path references 15 | } 16 | 17 | /** 18 | * Types of asset references 19 | */ 20 | export enum AssetType { 21 | INLINE = 'inline', 22 | PATH = 'path', 23 | } 24 | 25 | /** 26 | * Resolves asset references in the task description of a test scenario. 27 | * Loads inline assets and resolves paths for path assets. 28 | * 29 | * @param taskDescription - The task description string containing asset references. 30 | * @param filePath - The path to the feature behavior file (used to resolve relative asset paths). 31 | * @returns An object containing: 32 | * - processedDescription: The task description with inline asset references replaced by their content, and path references replaced by their absolute paths. 33 | * - assets: A record of AssetReference objects for each asset found in the description. 34 | */ 35 | export async function resolveAssetsInDescription( 36 | taskDescription: string, 37 | filePath: string 38 | ): Promise<{ 39 | processedDescription: string; 40 | assets: Record; 41 | }> { 42 | let processedDescription = taskDescription; 43 | const assets: Record = {}; 44 | 45 | // Process inline asset references: [[asset:name]] 46 | const inlineAssetRegex = /\[\[asset:([^\]]+)\]\]/g; 47 | let match; 48 | let tempDescription = taskDescription; 49 | 50 | while ((match = inlineAssetRegex.exec(tempDescription)) !== null) { 51 | const assetName = match[1]; 52 | const assetPath = path.join(path.dirname(filePath), path.basename(filePath, '.md'), assetName); 53 | 54 | try { 55 | // Load the asset content 56 | const assetContent = await readFile(assetPath, 'utf8'); 57 | 58 | // Store the asset reference 59 | assets[assetName] = { 60 | type: AssetType.INLINE, 61 | name: assetName, 62 | content: assetContent, 63 | }; 64 | 65 | // Replace the reference with the content in the task description 66 | processedDescription = processedDescription.replace(`[[asset:${assetName}]]`, assetContent); 67 | } catch (error) { 68 | console.error(`Error loading inline asset ${assetName} for ${filePath}:`, error); 69 | // Replace with error message 70 | processedDescription = processedDescription.replace( 71 | `[[asset:${assetName}]]`, 72 | `[Asset not found: ${assetName}]` 73 | ); 74 | } 75 | } 76 | 77 | // Process path references: {{path:name}} 78 | const pathAssetRegex = /\{\{path:([^}]+)\}\}/g; 79 | tempDescription = processedDescription; // Use updated description for new regex 80 | 81 | while ((match = pathAssetRegex.exec(tempDescription)) !== null) { 82 | const assetName = match[1]; 83 | const assetRelativePath = path.join(path.basename(filePath, '.md'), assetName); 84 | const assetAbsolutePath = path.resolve(path.dirname(filePath), assetRelativePath); 85 | 86 | // Check if the file exists 87 | try { 88 | await fs.promises.access(assetAbsolutePath, fs.constants.F_OK); 89 | 90 | // Store the asset reference 91 | assets[assetName] = { 92 | type: AssetType.PATH, 93 | name: assetName, 94 | path: assetAbsolutePath, 95 | }; 96 | 97 | // Replace the reference with the absolute path in the task description 98 | processedDescription = processedDescription.replace( 99 | `{{path:${assetName}}}`, 100 | assetAbsolutePath 101 | ); 102 | } catch (error) { 103 | console.error(`Error resolving path asset ${assetName} for ${filePath}:`, error); 104 | // Replace with error message 105 | processedDescription = processedDescription.replace( 106 | `{{path:${assetName}}}`, 107 | `[Asset path not found: ${assetName}]` 108 | ); 109 | } 110 | } 111 | 112 | return { 113 | processedDescription, 114 | assets, 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /src/utils/execAsync.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility module that provides a Promise-based wrapper around Node's exec function. 3 | * This allows for easier async/await usage of child process execution throughout the codebase. 4 | * 5 | * Usage: 6 | * ```typescript 7 | * try { 8 | * const { stdout, stderr } = await execAsync('some command'); 9 | * console.log('Output:', stdout); 10 | * } catch (error) { 11 | * console.error('Failed:', error); 12 | * } 13 | * ``` 14 | */ 15 | 16 | import { exec } from 'node:child_process'; 17 | import { promisify } from 'node:util'; 18 | 19 | // Convert the callback-based exec function to a Promise-based one 20 | export const execAsync = promisify(exec); 21 | -------------------------------------------------------------------------------- /src/utils/exhaustiveMatchGuard.ts: -------------------------------------------------------------------------------- 1 | export function exhaustiveMatchGuard(value: never, customError?: string) { 2 | throw new Error(`Unhandled case: ${value} ${customError ? `: ${customError}` : ''}`); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/messageChunker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the nearest valid split point before the target index. 3 | * Prioritizes sentence boundaries (period, exclamation, question mark + whitespace, or newline), 4 | * then falls back to secondary boundaries (comma, semicolon), and finally spaces. 5 | * If no valid split point is found, returns the target index itself. 6 | */ 7 | function findNearestSplitPoint(text: string, targetIndex: number): number { 8 | // Primary split points: sentence boundaries with one or more whitespace characters 9 | const sentenceSplitRegex = /[.!?]\s+|\n/g; 10 | let lastMatch = 0; 11 | let match; 12 | 13 | while ((match = sentenceSplitRegex.exec(text)) !== null) { 14 | if (match.index > targetIndex) { 15 | break; 16 | } 17 | lastMatch = match.index + match[0].length; 18 | } 19 | 20 | // If no sentence boundary found, try secondary split points (comma, semicolon) or spaces 21 | if (lastMatch <= 0) { 22 | const secondarySplitRegex = /[,;]\s+|\s/g; 23 | while ((match = secondarySplitRegex.exec(text)) !== null) { 24 | if (match.index > targetIndex) { 25 | break; 26 | } 27 | lastMatch = match.index + match[0].length; 28 | } 29 | // If still no split point found, force split at target index 30 | if (lastMatch <= 0) { 31 | return targetIndex; 32 | } 33 | } 34 | 35 | return lastMatch; 36 | } 37 | 38 | /** 39 | * Splits a message into chunks that are within the specified character limit. 40 | * Uses an iterative approach to find good split points at sentence boundaries, 41 | * falling back to secondary boundaries (comma, semicolon) and spaces if needed. 42 | * As a last resort, will force split at the character limit. 43 | */ 44 | export function chunkMessage(message: string, limit: number): string[] { 45 | if (message.length <= limit) { 46 | return [message]; 47 | } 48 | 49 | const chunks: string[] = []; 50 | let remainingText = message; 51 | 52 | while (remainingText.length > 0) { 53 | if (remainingText.length <= limit) { 54 | chunks.push(remainingText); 55 | break; 56 | } 57 | 58 | // Find the best split point within the limit 59 | const splitIndex = findNearestSplitPoint(remainingText, limit); 60 | 61 | // If no good split point found, force split at the limit 62 | const actualSplitIndex = splitIndex > 0 ? splitIndex : limit; 63 | 64 | // Extract the chunk, trim whitespace, and add to chunks if non-empty 65 | const chunk = remainingText.slice(0, actualSplitIndex).trim(); 66 | if (chunk) { 67 | chunks.push(chunk); 68 | } 69 | 70 | // Update remaining text for next iteration 71 | remainingText = remainingText.slice(actualSplitIndex).trim(); 72 | } 73 | 74 | return chunks; 75 | } 76 | -------------------------------------------------------------------------------- /src/utils/once.ts: -------------------------------------------------------------------------------- 1 | export function once(fn: () => T): () => T { 2 | let result: T; 3 | let func: (() => T) | null = fn; // Store fn in a variable that can be nulled 4 | 5 | return () => { 6 | if (func) { 7 | // Check if func is still defined 8 | result = func(); 9 | func = null; // Nullify func after first execution 10 | } 11 | return result; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/output.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { promisify } from 'util'; 3 | import path from 'path'; 4 | 5 | const writeFile = promisify(fs.writeFile); 6 | const mkdir = promisify(fs.mkdir); 7 | 8 | /** 9 | * Output text to standard output and optionally save to a file 10 | * This function is used by commands to output content 11 | * 12 | * @param text - The text to output 13 | * @param options - Optional configuration 14 | * @param options.saveTo - Path to save output to (in addition to stdout) 15 | * @param options.quiet - Suppress stdout output (only useful with saveTo) 16 | * @param prefix - Optional prefix to add to the text 17 | */ 18 | export async function yieldOutput( 19 | text: string, 20 | options: { saveTo?: string; quiet?: boolean; appendTo?: boolean }, 21 | prefix?: string 22 | ): Promise { 23 | // Format text with prefix if provided 24 | const outputText = prefix ? `[${prefix}] ${text}` : text; 25 | 26 | // Output to stdout unless quiet is true 27 | if (!options?.quiet) { 28 | process.stdout.write(outputText); 29 | } 30 | 31 | // Save to file if saveTo is specified 32 | if (options?.saveTo) { 33 | try { 34 | // Create directory if it doesn't exist 35 | const dir = path.dirname(options.saveTo); 36 | await mkdir(dir, { recursive: true }); 37 | 38 | // Write to file (append if appendTo is true) 39 | const flag = options.appendTo ? 'a' : 'w'; 40 | await writeFile(options.saveTo, outputText, { flag }); 41 | } catch (error) { 42 | console.error(`Error saving output to ${options.saveTo}:`, error); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * Function to handle file output in a consistent manner 49 | * 50 | * @param outputPath - Path to save the file 51 | * @param content - Content to write to the file 52 | * @param type - Optional content type for logging 53 | */ 54 | export async function saveToFile( 55 | outputPath: string, 56 | content: string, 57 | type: string = 'output' 58 | ): Promise { 59 | try { 60 | // Create directory if it doesn't exist 61 | const dir = path.dirname(outputPath); 62 | await mkdir(dir, { recursive: true }); 63 | 64 | // Write content to file 65 | await writeFile(outputPath, content); 66 | console.log(`${type} saved to: ${outputPath}`); 67 | } catch (error) { 68 | console.error(`Error saving ${type} to ${outputPath}:`, error); 69 | throw error; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/providerAvailability.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from '../types'; 2 | 3 | interface ProviderInfo { 4 | provider: Provider; 5 | available: boolean; 6 | defaultModel?: string; 7 | } 8 | 9 | // Default models for each provider when none specified in config 10 | const DEFAULT_MODELS: Record = { 11 | perplexity: 'sonar-pro', 12 | gemini: 'gemini-2.5-pro-preview', 13 | openai: 'o3-mini', 14 | anthropic: 'claude-sonnet-4-20250514', 15 | openrouter: 'anthropic/claude-sonnet-4', 16 | modelbox: 'anthropic/claude-sonnet-4', 17 | xai: 'grok-3-mini-latest', 18 | }; 19 | 20 | // Provider preference order for each command type 21 | export const PROVIDER_PREFERENCE: Record = { 22 | web: ['perplexity', 'gemini', 'modelbox', 'openrouter'], 23 | repo: ['gemini', 'modelbox', 'openrouter', 'openai', 'perplexity', 'anthropic', 'xai'], 24 | plan_file: ['gemini', 'modelbox', 'openrouter', 'openai', 'perplexity', 'anthropic', 'xai'], 25 | plan_thinking: ['openai', 'modelbox', 'openrouter', 'gemini', 'anthropic', 'perplexity', 'xai'], 26 | doc: ['gemini', 'modelbox', 'openrouter', 'openai', 'perplexity', 'anthropic', 'xai'], 27 | ask: ['openai', 'modelbox', 'openrouter', 'gemini', 'anthropic', 'perplexity'], 28 | browser: ['anthropic', 'openai', 'modelbox', 'openrouter', 'gemini', 'perplexity'], 29 | }; 30 | 31 | export function getDefaultModel(provider: Provider): string { 32 | return DEFAULT_MODELS[provider]; 33 | } 34 | 35 | export function getAllProviders(): ProviderInfo[] { 36 | return [ 37 | { 38 | provider: 'perplexity', 39 | available: !!process.env.PERPLEXITY_API_KEY, 40 | defaultModel: DEFAULT_MODELS.perplexity, 41 | }, 42 | { 43 | provider: 'gemini', 44 | available: !!process.env.GEMINI_API_KEY, 45 | defaultModel: DEFAULT_MODELS.gemini, 46 | }, 47 | { 48 | provider: 'openai', 49 | available: !!process.env.OPENAI_API_KEY, 50 | defaultModel: DEFAULT_MODELS.openai, 51 | }, 52 | { 53 | provider: 'anthropic', 54 | available: !!process.env.ANTHROPIC_API_KEY, 55 | defaultModel: DEFAULT_MODELS.anthropic, 56 | }, 57 | { 58 | provider: 'openrouter', 59 | available: !!process.env.OPENROUTER_API_KEY, 60 | defaultModel: DEFAULT_MODELS.openrouter, 61 | }, 62 | { 63 | provider: 'modelbox', 64 | available: !!process.env.MODELBOX_API_KEY, 65 | defaultModel: DEFAULT_MODELS.modelbox, 66 | }, 67 | { 68 | provider: 'xai', 69 | available: !!process.env.XAI_API_KEY, 70 | defaultModel: DEFAULT_MODELS.xai, 71 | }, 72 | ]; 73 | } 74 | 75 | export function getProviderInfo(provider: string): ProviderInfo | undefined { 76 | return getAllProviders().find((p) => p.provider === provider); 77 | } 78 | 79 | export function isProviderAvailable(provider: string): boolean { 80 | return !!getProviderInfo(provider)?.available; 81 | } 82 | 83 | export function getAvailableProviders(): ProviderInfo[] { 84 | return getAllProviders().filter((p) => p.available); 85 | } 86 | 87 | export function getNextAvailableProvider( 88 | commandType: keyof typeof PROVIDER_PREFERENCE, 89 | currentProvider?: Provider 90 | ): Provider | undefined { 91 | const preferenceOrder = PROVIDER_PREFERENCE[commandType]; 92 | if (!preferenceOrder) { 93 | throw new Error(`Unknown command type: ${commandType}`); 94 | } 95 | 96 | const availableProviders = getAllProviders(); 97 | 98 | // If currentProvider is specified, start looking from the next provider in the preference order 99 | const startIndex = currentProvider ? preferenceOrder.indexOf(currentProvider) + 1 : 0; 100 | 101 | // Look through remaining providers in preference order 102 | for (let i = startIndex; i < preferenceOrder.length; i++) { 103 | const provider = preferenceOrder[i]; 104 | const providerInfo = availableProviders.find((p) => p.provider === provider); 105 | if (providerInfo?.available) { 106 | return provider; 107 | } else { 108 | console.log(`Provider ${provider} is not available`); 109 | } 110 | } 111 | 112 | return undefined; 113 | } 114 | -------------------------------------------------------------------------------- /src/utils/stringSimilarity.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculate Levenshtein distance between two strings 3 | */ 4 | function levenshteinDistance(str1: string, str2: string): number { 5 | const m = str1.length; 6 | const n = str2.length; 7 | const dp: number[][] = Array(m + 1) 8 | .fill(0) 9 | .map(() => Array(n + 1).fill(0)); 10 | 11 | for (let i = 0; i <= m; i++) dp[i][0] = i; 12 | for (let j = 0; j <= n; j++) dp[0][j] = j; 13 | 14 | for (let i = 1; i <= m; i++) { 15 | for (let j = 1; j <= n; j++) { 16 | if (str1[i - 1] === str2[j - 1]) { 17 | dp[i][j] = dp[i - 1][j - 1]; 18 | } else { 19 | dp[i][j] = 20 | 1 + 21 | Math.min( 22 | dp[i - 1][j], // deletion 23 | dp[i][j - 1], // insertion 24 | dp[i - 1][j - 1] // substitution 25 | ); 26 | } 27 | } 28 | } 29 | return dp[m][n]; 30 | } 31 | 32 | /** 33 | * Calculate string similarity score between 0 and 1 34 | * 1 means strings are identical, 0 means completely different 35 | */ 36 | export function stringSimilarity(str1: string, str2: string): number { 37 | const len1 = str1.length; 38 | const len2 = str2.length; 39 | const maxLen = Math.max(len1, len2); 40 | if (maxLen === 0) return 1; 41 | 42 | const distance = levenshteinDistance(str1.toLowerCase(), str2.toLowerCase()); 43 | return 1 - distance / maxLen; 44 | } 45 | 46 | /** 47 | * Find similar models from a list of available models 48 | * Returns top 5 most similar models 49 | */ 50 | export function getSimilarModels(model: string, availableModels: Set): string[] { 51 | const modelParts = model.split('/', 2); 52 | const provider = modelParts.length > 1 ? modelParts[0] : null; 53 | const modelName = modelParts.length > 1 ? modelParts[1] : model; 54 | 55 | // Find models from the same provider if provider is provided 56 | const similarModels = Array.from(availableModels).filter((m) => { 57 | if (provider) { 58 | const [mProvider] = m.split('/'); 59 | return mProvider === provider; 60 | } 61 | return true; 62 | }); 63 | 64 | // Sort by similarity to the requested model name 65 | return similarModels 66 | .sort((a, b) => { 67 | const [, aName] = provider ? a.split('/') : [null, a]; 68 | const [, bName] = provider ? b.split('/') : [null, b]; 69 | const aSimilarity = stringSimilarity(modelName, aName); 70 | const bSimilarity = stringSimilarity(modelName, bName); 71 | return bSimilarity - aSimilarity; 72 | }) 73 | .slice(0, 5); // Return top 5 most similar models 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/versionUtils.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, existsSync } from 'node:fs'; 2 | import { join, dirname, resolve } from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { exec } from 'node:child_process'; 5 | import { promisify } from 'node:util'; 6 | import consola from 'consola'; 7 | 8 | const execAsync = promisify(exec); 9 | const __dirname = dirname(fileURLToPath(import.meta.url)); 10 | 11 | export interface VersionInfo { 12 | current: string; 13 | latest: string | null; 14 | isOutdated: boolean; 15 | } 16 | 17 | /** 18 | * Gets the currently installed version of vibe-tools by searching upwards 19 | * for a package.json file with the name "vibe-tools". 20 | */ 21 | export function getCurrentVersion(): string { 22 | let currentDir = __dirname; 23 | let attempts = 0; 24 | const maxAttempts = 5; // Prevent infinite loops 25 | 26 | while (attempts < maxAttempts) { 27 | const packageJsonPath = join(currentDir, 'package.json'); 28 | if (existsSync(packageJsonPath)) { 29 | try { 30 | const packageJsonContent = readFileSync(packageJsonPath, 'utf-8'); 31 | const packageJson = JSON.parse(packageJsonContent); 32 | 33 | if (packageJson.name === 'vibe-tools') { 34 | consola.debug('Found vibe-tools package.json at:', packageJsonPath); 35 | return packageJson.version; 36 | } 37 | } catch (error) { 38 | // Ignore errors reading/parsing intermediate package.json files 39 | consola.debug('Error reading/parsing intermediate package.json:', packageJsonPath, error); 40 | } 41 | } 42 | 43 | const parentDir = resolve(currentDir, '..'); 44 | // Stop if we have reached the root directory 45 | if (parentDir === currentDir) { 46 | break; 47 | } 48 | currentDir = parentDir; 49 | attempts++; 50 | } 51 | 52 | consola.error('Could not find vibe-tools package.json by searching upwards from', __dirname); 53 | return '0.0.0'; // Fallback version 54 | } 55 | 56 | /** 57 | * Gets the latest available version of vibe-tools from the NPM registry. 58 | * Uses `npm view vibe-tools version`. 59 | */ 60 | export async function getLatestVersion(): Promise { 61 | try { 62 | const { stdout } = await execAsync('npm view vibe-tools version'); 63 | return stdout.trim(); 64 | } catch (error) { 65 | consola.warn('Failed to fetch latest version from NPM:', error); 66 | return null; // Indicate failure to fetch 67 | } 68 | } 69 | 70 | /** 71 | * Checks if the currently installed version is outdated compared to the latest NPM version. 72 | * Note: This uses simple string comparison. For robust comparison (e.g., handling pre-releases), 73 | * a library like 'semver' would be better, but sticking to simplicity for now. 74 | */ 75 | export async function checkPackageVersion(): Promise { 76 | try { 77 | const current = getCurrentVersion(); 78 | // If we couldn't even get the current version, don't proceed with check/update 79 | if (current === '0.0.0') { 80 | consola.warn('Could not determine current package version. Skipping update check.'); 81 | return { current: '0.0.0', latest: null, isOutdated: false }; 82 | } 83 | 84 | const latest = await getLatestVersion(); 85 | 86 | if (latest) { 87 | const [currentMajor, currentMinor, currentPatchish] = current.split('.'); 88 | const currentPatch = currentPatchish?.split('-')[0]; 89 | 90 | const [latestMajor, latestMinor, latestPatchish] = latest.split('.'); 91 | const latestPatch = latestPatchish?.split('-')[0]; 92 | 93 | let isOutdated = false; 94 | if (parseInt(currentMajor) < parseInt(latestMajor)) { 95 | isOutdated = true; 96 | } 97 | if ( 98 | parseInt(currentMajor) === parseInt(latestMajor) && 99 | parseInt(currentMinor) < parseInt(latestMinor) 100 | ) { 101 | isOutdated = true; 102 | } 103 | if ( 104 | parseInt(currentMajor) === parseInt(latestMajor) && 105 | parseInt(currentMinor) === parseInt(latestMinor) && 106 | parseInt(currentPatch) < parseInt(latestPatch) 107 | ) { 108 | isOutdated = true; 109 | } 110 | 111 | return { 112 | current, 113 | latest, 114 | isOutdated, 115 | }; 116 | } else { 117 | consola.warn('Could not determine latest package version. Skipping update check.'); 118 | return { current, latest: null, isOutdated: false }; 119 | } 120 | } catch (error) { 121 | consola.warn('Error checking package version:', error); 122 | return { 123 | current: '0.0.0', // Ensure fallback on any check error 124 | latest: null, 125 | isOutdated: false, 126 | }; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/commands/ask/MODEL_LOOKUP.md: -------------------------------------------------------------------------------- 1 | # Model Lookup 2 | 3 | We use providers model listing endpoints where available to check the provided/configured model name. 4 | 5 | Getting model names exactly right is a pain and providers are often changing the names. So if the model name does not match we look for possible matches and in some cases we automatically switch 6 | 7 | ## These should succeed by automatically switching to an actual model 8 | 9 | we should be logging what is happening as well 10 | 11 | Automatic addition of missing provider 12 | 13 | ``` 14 | pnpm dev ask --provider modelbox --model gemini-2.5-flash-preview-05-20 "what is today's date" 15 | ``` 16 | 17 | Automatic Removal of -exp suffix 18 | 19 | ``` 20 | pnpm dev ask --provider gemini --model gemini-2.5-flash-preview-05-20 "what is today's date" 21 | ``` 22 | 23 | ``` 24 | pnpm dev ask --provider gemini --model gemini-2.5-flash-preview-05-20 "what is today's date" 25 | ``` 26 | 27 | Automatic detection of exact substring match 28 | 29 | ``` 30 | pnpm dev doc "what does this do" --debug --provider=openrouter --model=gemini-2.5-pro-preview 31 | ``` 32 | -------------------------------------------------------------------------------- /tests/commands/browser/animation-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Animation Test 5 | 99 | 100 | 101 |
102 |
0.0s
103 | 104 | 109 |
110 |
111 |
112 | 113 | 146 | 147 | -------------------------------------------------------------------------------- /tests/commands/browser/broken-form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Broken Form Demo 7 | 42 | 43 | 44 |

User Registration Form

45 |
46 |
47 | 48 | 49 |
Username must be at least 3 characters
50 |
51 |
52 | 53 | 54 |
Please enter a valid email
55 |
56 |
57 | 58 | 59 |
Password must be at least 6 characters
60 |
61 | 62 |
63 | 64 | 117 | 118 | -------------------------------------------------------------------------------- /tests/commands/browser/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Button Test 5 | 6 | 7 | 8 |
9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /tests/commands/browser/console-log-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Console Log Test 5 | 6 | 7 |

Console Log Test Page

8 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/commands/browser/console-log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Console Log Test 5 | 6 | 7 |

Console Log Test Page

8 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/commands/browser/console-network-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Console and Network Test 5 | 6 | 7 |

Console and Network Test

8 | 20 | 21 | -------------------------------------------------------------------------------- /tests/commands/browser/interactive-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Interactive Elements Test 5 | 11 | 12 | 13 |
14 |

Interactive Elements Test Page

15 | 16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 | 30 |
31 | 32 |
33 | 34 | 40 |
41 | 42 |
43 | 44 | 45 | Need Help? 46 |
47 |
48 | 49 | 65 | 66 | -------------------------------------------------------------------------------- /tests/commands/browser/network-request-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Favicon Test 5 | 6 | 7 | 8 | 9 |

Favicon Test Page

10 | This image should not exist 11 | This one too 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/commands/browser/serve.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Test server for browser command testing 3 | * 4 | * Usage: 5 | * 1. Run with: pnpm serve-test 6 | * 2. Server starts at http://localhost:3000 7 | * 3. Place test HTML files in tests/commands/browser/ 8 | * 4. Access files at http://localhost:3000/filename.html 9 | * 10 | * Default test page: http://localhost:3000 (serves console-log.html) 11 | */ 12 | 13 | import { join } from 'node:path'; 14 | 15 | const port = process.env['PORT'] || 3000; 16 | console.log(`Starting server on http://localhost:${port}`); 17 | 18 | Bun.serve({ 19 | port, 20 | async fetch(req) { 21 | const url = new URL(req.url); 22 | const filePath = url.pathname === '/' ? 'console-log.html' : url.pathname.slice(1); 23 | const fullPath = join(import.meta.dir, filePath); 24 | const file = Bun.file(fullPath); 25 | 26 | try { 27 | if (await file.exists()) { 28 | return new Response(file); 29 | } 30 | return new Response('404 Not Found', { status: 404 }); 31 | } catch (error) { 32 | console.error(`Error serving ${filePath}:`, error); 33 | return new Response('500 Internal Server Error', { status: 500 }); 34 | } 35 | } 36 | }); -------------------------------------------------------------------------------- /tests/commands/browser/test-browser-persistence.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Browser Command Test Page 5 | 23 | 24 | 25 |

Test Page for Browser Commands

26 | 27 |
28 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 |
37 | 38 |
39 | 40 | 54 | 55 | -------------------------------------------------------------------------------- /tests/commands/browser/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Browser Command Test Page 5 | 23 | 24 | 25 |

Test Page for Browser Commands

26 | 27 |
28 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 |
37 | 38 |
39 | 40 | 54 | 55 | -------------------------------------------------------------------------------- /tests/feature-behaviors/ask/ask-command/scenario9-long-query.txt: -------------------------------------------------------------------------------- 1 | I'm conducting research on the intersection of artificial intelligence and climate science, particularly focusing on how machine learning models can be optimized to process and analyze the enormous datasets involved in climate modeling. Could you elaborate on the current state of AI applications in climate science, including specific neural network architectures that have proven effective for tasks like predicting extreme weather events, modeling sea level rise, and analyzing atmospheric carbon patterns? Additionally, I'm interested in understanding the computational challenges faced when training these models on climate datasets, which often include multiple variables across different spatial and temporal scales. How are researchers addressing issues of model interpretability, given that climate policy decisions may need to be based on these AI predictions? Finally, what ethical considerations should be taken into account when developing AI systems for climate science, particularly regarding data privacy, potential biases in training data from historical measurements, and the environmental impact of running computationally intensive models? Please provide examples of successful projects and current research directions in this interdisciplinary field. -------------------------------------------------------------------------------- /tests/feature-behaviors/ask/ask-command/simple-query: -------------------------------------------------------------------------------- 1 | What is the capital of France? Please provide some history about its significance as a cultural and political center. -------------------------------------------------------------------------------- /tests/feature-behaviors/ask/reasoning-effort-tests.md: -------------------------------------------------------------------------------- 1 | # Feature Behavior: Reasoning Effort Parameter 2 | 3 | ## Description 4 | 5 | vibe-tools should support the reasoning effort parameter for OpenAI models and extended thinking for Anthropic models. This feature allows users to adjust the level of reasoning depth that models apply when answering questions, which can significantly improve the quality of responses for complex questions. 6 | 7 | ## Test Scenarios 8 | 9 | ### Scenario 1: OpenRouter with OpenAI Model and High Reasoning 10 | 11 | **Tags:** openrouter, extended-reasoning 12 | **Task Description:** 13 | Use vibe-tools to ask a complex logical reasoning question using OpenRouter with an OpenAI model and high reasoning effort. 14 | 15 | Use this exact query: "Explain how merge sort works, its time complexity, and provide a step-by-step walkthrough of how it would sort the array [38, 27, 43, 3, 9, 82, 10]. Analyze why it's more efficient than bubble sort for large datasets." 16 | 17 | **Expected Behavior:** 18 | 19 | - The AI agent should use the ask command with OpenRouter provider, "openai/o3-mini" model, and high reasoning effort 20 | - The response should include a detailed explanation of merge sort with time complexity analysis 21 | - The command should complete successfully without errors 22 | - The response should show evidence of deep reasoning (comprehensive explanation, step-by-step walkthrough, clear comparisons) 23 | 24 | **Success Criteria:** 25 | 26 | - AI agent correctly uses ask command with OpenRouter provider and "openai/o3-mini" model 27 | - AI agent correctly sets the reasoning effort parameter to high 28 | - Response contains a detailed explanation of merge sort 29 | - Response includes step-by-step walkthrough of sorting the specific array 30 | - Response contains time complexity analysis and comparison with bubble sort 31 | - Response shows evidence of more thorough reasoning compared to default settings 32 | - No error messages are displayed 33 | - Command completes within a reasonable time 34 | 35 | ### Scenario 2: Incompatible Model with Reasoning Effort (Warning Message) 36 | 37 | **Tags:** extended-reasoning, error-handling, anthropic 38 | **Task Description:** 39 | Use vibe-tools to ask a question using a model that does not support reasoning effort, such as Anthropic with claude-3-5-haiku-latest model, and attempt to use reasoning effort parameter. 40 | 41 | Use this exact query: "A cryptographic hash function takes an input and returns a fixed-size string of bytes. Explain what properties make a good cryptographic hash function, and analyze how SHA-256 achieves these properties. Discuss at least three real-world applications where SHA-256 is commonly used." 42 | 43 | **Expected Behavior:** 44 | 45 | - The AI agent should use the ask command with Anthropic provider, claude-3-5-haiku-latest model, and high reasoning effort 46 | - The command should execute but log a warning that the model does not support reasoning effort 47 | - The response should still provide an answer to the question 48 | - The command should complete successfully despite the incompatible parameter 49 | 50 | **Success Criteria:** 51 | 52 | - AI agent correctly uses ask command with Anthropic provider and claude-3-5-haiku-latest model 53 | - AI agent correctly sets the reasoning effort parameter to high 54 | - Command outputs a warning message indicating the model does not support reasoning effort 55 | - Response still explains properties of cryptographic hash functions 56 | - Response successfully completes despite the incompatible parameter 57 | - Console output includes a message about ignoring the reasoning effort parameter 58 | - No errors that prevent command execution are displayed 59 | 60 | ### Scenario 3: Anthropic Model with Extended Thinking 61 | 62 | **Tags:** extended-reasoning, anthropic 63 | **Task Description:** 64 | Use vibe-tools to ask a complex critical analysis question using Anthropic provider with claude-sonnet-4 model and high reasoning effort. 65 | 66 | Use this exact query: "Compare and contrast three major frameworks for ethical AI development: utilitarian, deontological, and virtue ethics approaches. Analyze how each framework would address issues of privacy, bias, and automation of decision-making. Then, propose a hybrid framework that combines strengths from each approach while addressing their limitations." 67 | 68 | **Expected Behavior:** 69 | 70 | - The AI agent should use the ask command with Anthropic provider, claude-sonnet-4-20250514 model, and high reasoning effort 71 | - The response should include a comprehensive comparison of ethical frameworks with detailed analysis 72 | - The command should complete successfully without errors 73 | - The response should show evidence of extended thinking (thorough comparison, nuanced analysis, thoughtful hybrid proposal) 74 | 75 | **Success Criteria:** 76 | 77 | - AI agent correctly uses ask command with Anthropic provider and claude-sonnet-4-20250514 model 78 | - AI agent correctly sets the reasoning effort parameter to high (maps to extended thinking) 79 | - Response includes comparison of three ethical frameworks 80 | - Response analyzes how each framework addresses specific ethical issues 81 | - Response proposes a hybrid framework with strengths from each approach 82 | - Response shows evidence of deeper analysis with extended thinking enabled 83 | - Console output includes a message about using extended thinking with appropriate token budget 84 | - No error messages are displayed 85 | - Command completes within a reasonable time 86 | -------------------------------------------------------------------------------- /tests/feature-behaviors/doc/doc-command/doc-repomix-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": { 3 | "useGitignore": true, 4 | "useDefaultPatterns": true, 5 | "customPatterns": [ 6 | "**/node_modules/**", 7 | "**/dist/**", 8 | "**/build/**", 9 | "**/*.test.*", 10 | "**/*.spec.*", 11 | "**/tests/**", 12 | "**/drafts/**", 13 | "**/.github/**" 14 | ] 15 | }, 16 | "include": [ 17 | "src/**/*", 18 | "README.md", 19 | "package.json", 20 | ".cursorrules", 21 | ".cursor/rules/*" 22 | ], 23 | "output": { 24 | "removeEmptyLines": true, 25 | "fileSummary": true, 26 | "directoryStructure": true, 27 | "showLineNumbers": false 28 | }, 29 | "tokenCount": { 30 | "encoding": "o200k_base" 31 | }, 32 | "security": { 33 | "enableSecurityCheck": true 34 | } 35 | } -------------------------------------------------------------------------------- /tests/feature-behaviors/doc/doc-command/doc-vibe-tools.config1.json: -------------------------------------------------------------------------------- 1 | { 2 | "doc": { 3 | "provider": "openai", 4 | "model": "gpt-4.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/feature-behaviors/doc/doc-command/doc-vibe-tools.config2.json: -------------------------------------------------------------------------------- 1 | { 2 | "doc": { 3 | "provider": "openrouter", 4 | "model": "google/gemini-2.5-flash-preview-05-20" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/feature-behaviors/mcp/mcp-command-edge-cases.md: -------------------------------------------------------------------------------- 1 | # Feature Behavior: Model Context Protocol Edge Cases 2 | 3 | ## Description 4 | Tests edge cases and error handling scenarios for vibe-tools MCP command interactions, focusing on server configuration, database access issues, and tool-specific edge cases. 5 | 6 | ## Test Scenarios 7 | 8 | ### Scenario 1: MCP Run with SQLite Server Override (Happy Path) 9 | **Tags:** parameters 10 | **Task Description:** 11 | Use vibe-tools to execute the SQLite MCP server's `read_query` tool with a server override parameter to query the test database at {{path:test.db}}. Run a SELECT query to get all users from the users table. 12 | 13 | **Expected Behavior:** 14 | - The AI agent should include the server override parameter pointing to the SQLite MCP server 15 | - The command should execute the `read_query` tool with the SELECT statement 16 | - The response should include the list of users from the database 17 | - The command should complete successfully without errors 18 | 19 | **Success Criteria:** 20 | - AI agent correctly includes the server override parameter for the SQLite MCP server 21 | - Command executes the `read_query` tool with a valid SQL SELECT statement 22 | - Response includes all users from the test database 23 | - No error messages are displayed 24 | - Command completes successfully 25 | 26 | ### Scenario 2: MCP Run with Invalid Table Schema (Error Handling) 27 | **Task Description:** 28 | Attempt to use vibe-tools to execute the SQLite MCP server's `create_table` tool with an invalid table schema (missing required fields). When running this command include instructions not to retry the table creation if it fails. 29 | 30 | **Expected Behavior:** 31 | - The command should fail with a clear error message 32 | - Error message should indicate the schema validation error 33 | - The error should be handled gracefully without crashing 34 | 35 | **Success Criteria:** 36 | - AI agent recognizes the schema validation error 37 | - Command fails gracefully with informative error message 38 | - Error message indicates which schema fields are invalid/missing 39 | - No partial or corrupted output is generated 40 | 41 | ### Scenario 3: MCP Run with Simple Pagination (Edge Case) 42 | **Tags:** performance 43 | **Task Description:** 44 | Use vibe-tools to execute the SQLite MCP server's `read_query` tool to test basic pagination by: 45 | 1. Getting the total count of users 46 | 2. Retrieving the first page (LIMIT 2) 47 | 3. Retrieving the second page (LIMIT 2 OFFSET 2) 48 | 49 | **Expected Behavior:** 50 | - The command should handle basic pagination correctly 51 | - Each page should return exactly 2 users 52 | - The second page should start after the first page 53 | - Each command should complete quickly 54 | 55 | **Success Criteria:** 56 | - AI agent correctly executes the count and paginated queries 57 | - First page shows users 1-2 58 | - Second page shows user 3 59 | - Each query completes in under 5 seconds 60 | - No error messages are displayed 61 | -------------------------------------------------------------------------------- /tests/feature-behaviors/mcp/mcp-command-edge-cases/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eastlondoner/cursor-tools/703f413e540d23f930e3b622dcde8ca93f35c89c/tests/feature-behaviors/mcp/mcp-command-edge-cases/test.db -------------------------------------------------------------------------------- /tests/feature-behaviors/mcp/mcp-command/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eastlondoner/cursor-tools/703f413e540d23f930e3b622dcde8ca93f35c89c/tests/feature-behaviors/mcp/mcp-command/test.db -------------------------------------------------------------------------------- /tests/feature-behaviors/plan/plan-command/plan-repomix-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": { 3 | "useGitignore": true, 4 | "useDefaultPatterns": true, 5 | "customPatterns": [ 6 | "**/node_modules/**", 7 | "**/dist/**", 8 | "**/build/**", 9 | "**/*.test.*", 10 | "**/*.spec.*", 11 | "**/tests/**", 12 | "**/docs/**" 13 | ] 14 | }, 15 | "include": [ 16 | "src/commands/**/*", 17 | "src/utils/**/*", 18 | "src/providers/**/*", 19 | ".cursorrules", 20 | ".cursor/rules/*" 21 | ], 22 | "output": { 23 | "removeEmptyLines": true, 24 | "fileSummary": true, 25 | "directoryStructure": true 26 | }, 27 | "tokenCount": { 28 | "encoding": "o200k_base" 29 | } 30 | } -------------------------------------------------------------------------------- /tests/feature-behaviors/repo/repo-command/basic-repomix-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": { 3 | "useGitignore": true, 4 | "useDefaultPatterns": true, 5 | "customPatterns": [ 6 | "**/node_modules/**", 7 | "**/dist/**", 8 | "**/build/**", 9 | "**/*.test.*", 10 | "**/*.spec.*", 11 | "**/tests/**" 12 | ] 13 | }, 14 | "include": [ 15 | "**/*", 16 | ".cursorrules", 17 | ".cursor/rules/*" 18 | ], 19 | "output": { 20 | "removeEmptyLines": true, 21 | "fileSummary": true, 22 | "directoryStructure": true 23 | }, 24 | "tokenCount": { 25 | "encoding": "o200k_base" 26 | } 27 | } -------------------------------------------------------------------------------- /tests/feature-behaviors/repo/repo-command/create-repomixignore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script creates a .repomixignore file with the specified content 3 | # Usage: ./create-repomixignore.sh "pattern" 4 | 5 | PATTERN="${1:-src/**}" 6 | 7 | sqlite3 :memory: <