diff --git a/.github/workflows/issue-comment.yml b/.github/workflows/issue-comment.yml index 5e64739..29b0b7a 100644 --- a/.github/workflows/issue-comment.yml +++ b/.github/workflows/issue-comment.yml @@ -35,13 +35,19 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + - uses: pnpm/action-setup@v2 with: version: ${{ env.PNPM_VERSION }} - - uses: actions/setup-node@v4 - with: - node-version-file: .nvmrc + - name: Install dependencies + run: pnpm install + + - name: Install browsers + run: cd packages/agent && pnpm exec playwright install --with-deps chromium - name: Configure Git run: | @@ -54,13 +60,11 @@ jobs: # Auth GitHub CLI with the token - name: Configure GitHub CLI run: | - # First try with GITHUB_TOKEN - echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token + echo "${{ secrets.GH_PAT }}" | gh auth login --with-token # Verify auth status gh auth status - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} run: | echo "Running MyCoder for issue #${{ github.event.issue.number }} with prompt: ${{ steps.extract-prompt.outputs.prompt }}" diff --git a/.releaserc.json b/.releaserc.json deleted file mode 100644 index 11f8dd1..0000000 --- a/.releaserc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "branches": ["release"], - "extends": "semantic-release-monorepo", - "plugins": [ - "@semantic-release/commit-analyzer", - "@semantic-release/release-notes-generator", - "@semantic-release/changelog", - "@semantic-release/npm", - [ - "@semantic-release/git", - { - "assets": ["package.json", "CHANGELOG.md"], - "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - } - ], - "@semantic-release/github" - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 7a97d1d..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,49 +0,0 @@ -## [0.10.1](https://github.com/drivecore/mycoder/compare/v0.10.0...v0.10.1) (2025-03-11) - -### Bug Fixes - -- token caching ([5972e59](https://github.com/drivecore/mycoder/commit/5972e59ab572040e564d1756ab8a5625215e14dc)) - -# [0.10.0](https://github.com/drivecore/mycoder/compare/v0.9.0...v0.10.0) (2025-03-11) - -### Bug Fixes - -- add deepmerge to cli package.json ([ab66377](https://github.com/drivecore/mycoder/commit/ab66377342c9f23fa874d2776e73d365141e8801)) -- update hierarchical configuration system to fix failing tests ([93d949c](https://github.com/drivecore/mycoder/commit/93d949c03b7ebe96bad36713f6476c38d2a35224)) - -### Features - -- add back token tracking, system prompt caching. ([ddc04ab](https://github.com/drivecore/mycoder/commit/ddc04ab0778eb2f571897e825c8d8ba17651db09)) -- add showStdIn and showStdout options to shellMessage and shellStart ([aed1b9f](https://github.com/drivecore/mycoder/commit/aed1b9f6ba489da19f2170c136861a7c80ad6e33)), closes [#167](https://github.com/drivecore/mycoder/issues/167) -- add token caching. issue 145 ([d78723b](https://github.com/drivecore/mycoder/commit/d78723bb6d0514110088caf7009e196e3f79769e)) -- implement hierarchical configuration system ([84d73d1](https://github.com/drivecore/mycoder/commit/84d73d1e6324670890a203f455fe257aeb6ed07a)), closes [#153](https://github.com/drivecore/mycoder/issues/153) - -# [0.9.0](https://github.com/drivecore/mycoder/compare/v0.8.0...v0.9.0) (2025-03-11) - -### Bug Fixes - -- don't save consent when using --userWarning=false ([41cf69d](https://github.com/drivecore/mycoder/commit/41cf69dee22acc31cd0f2aa9f80e36cd867fb20b)) - -### Features - -- add CLI options for automated usage scenarios ([00419bc](https://github.com/drivecore/mycoder/commit/00419bc3e060db6d0c18fc72e2d7b6957791c875)) - -# [0.8.0](https://github.com/drivecore/mycoder/compare/v0.7.0...v0.8.0) (2025-03-11) - -### Features - -- add --githubMode and --userPrompt as boolean CLI options that override config settings ([0390f94](https://github.com/drivecore/mycoder/commit/0390f94651e40de93a8cb9486a056a0b9cb2e165)) -- remove modelProvider and modelName - instant decrepation ([59834dc](https://github.com/drivecore/mycoder/commit/59834dcf932051a5c75624bd6f6ab12254f43769)) - -# [0.7.0](https://github.com/drivecore/mycoder/compare/v0.6.1...v0.7.0) (2025-03-10) - -### Bug Fixes - -- change where anthropic key is declared ([f6f72d3](https://github.com/drivecore/mycoder/commit/f6f72d3bc18a65fc775151cd375398aba230a06f)) -- ensure npm publish only happens on release branch ([ec352d6](https://github.com/drivecore/mycoder/commit/ec352d6956c717726ef388a07d88372c12b634a6)) - -### Features - -- add GitHub Action for issue comment commands ([136950f](https://github.com/drivecore/mycoder/commit/136950f4bd6d14e544bbd415ed313f7842a9b9a2)), closes [#162](https://github.com/drivecore/mycoder/issues/162) -- allow for generic /mycoder commands ([4b6608e](https://github.com/drivecore/mycoder/commit/4b6608e0b8e5f408eb5f12fe891657a5fb25bdb4)) -- **release:** implement conventional commits approach ([5878dd1](https://github.com/drivecore/mycoder/commit/5878dd1a56004eb8a994d40416d759553b022eb8)), closes [#140](https://github.com/drivecore/mycoder/issues/140) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e1ae07f..0642c85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,9 +72,13 @@ This project and everyone participating in it is governed by our Code of Conduct 4. Commit your changes: ```bash - git commit + pnpm commit ``` + This will launch an interactive prompt to help you create a properly formatted commit message. + + Alternatively, you can use the regular git commit command, but we recommend using `pnpm commit` for better guidance. + We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification for our commit messages: - **feat**: A new feature @@ -108,7 +112,7 @@ This project and everyone participating in it is governed by our Code of Conduct Closes #123 ``` - We have set up a commit message template and commitlint to help you follow this convention. + We have set up a commit message template, commitizen, and commitlint to help you follow this convention. Using `pnpm commit` will guide you through the process of creating a properly formatted commit message. 5. Push to your fork and create a Pull Request diff --git a/README.md b/README.md index 0ecc763..612c618 100644 --- a/README.md +++ b/README.md @@ -35,20 +35,54 @@ mycoder "Implement a React component that displays a list of items" mycoder -f prompt.txt # Disable user prompts for fully automated sessions -mycoder --enableUserPrompt false "Generate a basic Express.js server" +mycoder --userPrompt false "Generate a basic Express.js server" # or using the alias mycoder --userPrompt false "Generate a basic Express.js server" # Disable user consent warning and version upgrade check for automated environments mycoder --userWarning false --upgradeCheck false "Generate a basic Express.js server" -# Enable GitHub mode via CLI option (overrides config) +# Enable GitHub mode via CLI option (overrides config file) mycoder --githubMode "Work with GitHub issues and PRs" +``` + +## Configuration + +MyCoder is configured using a `mycoder.config.js` file in your project root, similar to ESLint and other modern JavaScript tools. This file exports a configuration object with your preferred settings. + +### Creating a Configuration File + +Create a `mycoder.config.js` file in your project root: -# Enable GitHub mode via config -mycoder config set githubMode true +```js +// mycoder.config.js +export default { + // GitHub integration + githubMode: true, + + // Browser settings + headless: true, + userSession: false, + pageFilter: 'none', // 'simple', 'none', or 'readability' + + // Model settings + provider: 'anthropic', + model: 'claude-3-7-sonnet-20250219', + maxTokens: 4096, + temperature: 0.7, + + // Custom settings + customPrompt: '', + profile: false, + tokenCache: true, + + // Ollama configuration (if using local models) + ollamaBaseUrl: 'http://localhost:11434', +}; ``` +CLI arguments will override settings in your configuration file. + ### GitHub Comment Commands MyCoder can be triggered directly from GitHub issue comments using the flexible `/mycoder` command: @@ -85,6 +119,9 @@ pnpm build # Run tests pnpm test + +# Create a commit with interactive prompt +pnpm commit ``` ## Release Process diff --git a/docs/tools/agent-tools.md b/docs/tools/agent-tools.md new file mode 100644 index 0000000..fab1cca --- /dev/null +++ b/docs/tools/agent-tools.md @@ -0,0 +1,130 @@ +# Agent Tools + +The agent tools provide ways to create and interact with sub-agents. There are two approaches available: + +1. The original `subAgent` tool (synchronous, blocking) +2. The new `agentStart` and `agentMessage` tools (asynchronous, non-blocking) + +## subAgent Tool + +The `subAgent` tool creates a sub-agent that runs synchronously until completion. The parent agent waits for the sub-agent to complete before continuing. + +```typescript +subAgent({ + description: "A brief description of the sub-agent's purpose", + goal: 'The main objective that the sub-agent needs to achieve', + projectContext: 'Context about the problem or environment', + workingDirectory: '/path/to/working/directory', // optional + relevantFilesDirectories: 'src/**/*.ts', // optional +}); +``` + +## agentStart and agentMessage Tools + +The `agentStart` and `agentMessage` tools provide an asynchronous approach to working with sub-agents. This allows the parent agent to: + +- Start multiple sub-agents in parallel +- Monitor sub-agent progress +- Provide guidance to sub-agents +- Terminate sub-agents if needed + +### agentStart + +The `agentStart` tool creates a sub-agent and immediately returns an instance ID. The sub-agent runs asynchronously in the background. + +```typescript +const { instanceId } = agentStart({ + description: "A brief description of the sub-agent's purpose", + goal: 'The main objective that the sub-agent needs to achieve', + projectContext: 'Context about the problem or environment', + workingDirectory: '/path/to/working/directory', // optional + relevantFilesDirectories: 'src/**/*.ts', // optional + userPrompt: false, // optional, default: false +}); +``` + +### agentMessage + +The `agentMessage` tool allows interaction with a running sub-agent. It can be used to check the agent's progress, provide guidance, or terminate the agent. + +```typescript +// Check agent progress +const { output, completed } = agentMessage({ + instanceId: 'agent-instance-id', + description: 'Checking agent progress', +}); + +// Provide guidance (note: guidance implementation is limited in the current version) +agentMessage({ + instanceId: 'agent-instance-id', + guidance: 'Focus on the task at hand and avoid unnecessary exploration', + description: 'Providing guidance to the agent', +}); + +// Terminate the agent +agentMessage({ + instanceId: 'agent-instance-id', + terminate: true, + description: 'Terminating the agent', +}); +``` + +## Example: Using agentStart and agentMessage to run multiple sub-agents in parallel + +```typescript +// Start multiple sub-agents +const agent1 = agentStart({ + description: 'Agent 1', + goal: 'Implement feature A', + projectContext: 'Project X', +}); + +const agent2 = agentStart({ + description: 'Agent 2', + goal: 'Implement feature B', + projectContext: 'Project X', +}); + +// Check progress of both agents +let agent1Completed = false; +let agent2Completed = false; + +while (!agent1Completed || !agent2Completed) { + if (!agent1Completed) { + const result1 = agentMessage({ + instanceId: agent1.instanceId, + description: 'Checking Agent 1 progress', + }); + agent1Completed = result1.completed; + + if (agent1Completed) { + console.log('Agent 1 completed with result:', result1.output); + } + } + + if (!agent2Completed) { + const result2 = agentMessage({ + instanceId: agent2.instanceId, + description: 'Checking Agent 2 progress', + }); + agent2Completed = result2.completed; + + if (agent2Completed) { + console.log('Agent 2 completed with result:', result2.output); + } + } + + // Wait before checking again + if (!agent1Completed || !agent2Completed) { + sleep({ seconds: 5 }); + } +} +``` + +## Choosing Between Approaches + +- Use `subAgent` for simpler tasks where blocking execution is acceptable +- Use `agentStart` and `agentMessage` for: + - Parallel execution of multiple sub-agents + - Tasks where you need to monitor progress + - Situations where you may need to provide guidance or terminate early diff --git a/lerna.json b/lerna.json deleted file mode 100644 index cb4c7eb..0000000 --- a/lerna.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "independent", - "npmClient": "pnpm", - "command": { - "publish": { - "conventionalCommits": true, - "message": "chore(release): publish" - } - }, - "packages": ["packages/*"] -} diff --git a/lerna.json.bak b/lerna.json.bak deleted file mode 100644 index 19efdaf..0000000 --- a/lerna.json.bak +++ /dev/null @@ -1 +0,0 @@ -{\n \"version\": \"independent\",\n \"npmClient\": \"pnpm\",\n \"command\": {\n \"publish\": {\n \"conventionalCommits\": true,\n \"message\": \"chore(release): publish\"\n }\n },\n \"packages\": [\"packages/*\"]\n} \ No newline at end of file diff --git a/mycoder.config.js b/mycoder.config.js new file mode 100644 index 0000000..e6877d4 --- /dev/null +++ b/mycoder.config.js @@ -0,0 +1,25 @@ +// mycoder.config.js +export default { + // GitHub integration + githubMode: true, + + // Browser settings + headless: true, + userSession: false, + pageFilter: 'none', // 'simple', 'none', or 'readability' + + // Model settings + provider: 'anthropic', + model: 'claude-3-7-sonnet-20250219', + //provider: 'openai', + //model: 'gpt-4o', + //provider: 'ollama', + //model: 'medragondot/Sky-T1-32B-Preview:latest', + maxTokens: 4096, + temperature: 0.7, + + // Custom settings + customPrompt: '', + profile: false, + tokenCache: true, +}; diff --git a/package.json b/package.json index 1b58c3f..ea0bc06 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "cloc": "pnpm exec cloc * --exclude-dir=node_modules,dist,.vinxi,.output", "gcloud-setup": "gcloud auth application-default login && gcloud config set account \"ben@drivecore.ai\" && gcloud config set project drivecore-primary && gcloud config set run/region us-central1", "cli": "cd packages/cli && node --no-deprecation bin/cli.js", + "commit": "cz", "prepare": "husky", "verify-release-config": "node scripts/verify-release-config.js", "release": "pnpm verify-release-config && pnpm -r --workspace-concurrency=1 exec -- pnpm exec semantic-release -e semantic-release-monorepo" @@ -34,7 +35,7 @@ "rimraf": "^6.0.1" }, "devDependencies": { - "@changesets/cli": "^2.28.1", + "@anolilab/semantic-release-pnpm": "^1.1.10", "@commitlint/cli": "^19.7.1", "@commitlint/config-conventional": "^19.7.1", "@eslint/js": "^9", @@ -43,6 +44,8 @@ "@semantic-release/github": "^11.0.1", "@typescript-eslint/eslint-plugin": "^8.23.0", "@typescript-eslint/parser": "^8.23.0", + "commitizen": "^4.3.1", + "cz-conventional-changelog": "^3.3.0", "eslint": "^9.0.0", "eslint-config-prettier": "^9", "eslint-import-resolver-typescript": "^3.8.3", @@ -57,6 +60,11 @@ "semantic-release-monorepo": "^8.0.2", "typescript-eslint": "^8.23.0" }, + "config": { + "commitizen": { + "path": "cz-conventional-changelog" + } + }, "pnpm": { "onlyBuiltDependencies": [ "@parcel/watcher", diff --git a/packages/agent/.releaserc.json b/packages/agent/.releaserc.json index 9c41120..5c32972 100644 --- a/packages/agent/.releaserc.json +++ b/packages/agent/.releaserc.json @@ -5,7 +5,7 @@ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", - "@semantic-release/npm", + "@anolilab/semantic-release-pnpm", [ "@semantic-release/git", { diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index a39368d..ea63537 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,21 +1,43 @@ -# mycoder-agent-v1.0.0 (2025-03-11) +# [mycoder-agent-v1.1.0](https://github.com/drivecore/mycoder/compare/mycoder-agent-v1.0.0...mycoder-agent-v1.1.0) (2025-03-12) ### Bug Fixes -* **monorepo:** implement semantic-release-monorepo for proper versioning of sub-packages ([96c6284](https://github.com/drivecore/mycoder/commit/96c62848fbc3a4c1c591f3fd6202486e6461c4f2)) -* only consider response empty if no text AND no tool calls ([#127](https://github.com/drivecore/mycoder/issues/127)) ([af20ec5](https://github.com/drivecore/mycoder/commit/af20ec54468afed49632306fe553b307ab3c4ba5)) -* Replace shell commands with Node.js APIs for cross-platform compatibility ([07b4c24](https://github.com/drivecore/mycoder/commit/07b4c24fa17d19c468a76404a367f6afc0005517)) -* token caching ([5972e59](https://github.com/drivecore/mycoder/commit/5972e59ab572040e564d1756ab8a5625215e14dc)) -* use maxTokens in generateTextProps ([bfb9da9](https://github.com/drivecore/mycoder/commit/bfb9da9804d61840344e93cc5bea809e8e16f2ec)) +* convert absolute paths to relative paths in textEditor log output ([a5ea845](https://github.com/drivecore/mycoder/commit/a5ea845c32bc569cda4330f59f1bf1553a236aea)) +* implement resource cleanup to prevent CLI hanging issue ([d33e729](https://github.com/drivecore/mycoder/commit/d33e7298686a30661ee8b36f2fdffb16f5f3da71)), closes [#141](https://github.com/drivecore/mycoder/issues/141) +* llm choice working well for openai, anthropic and ollama ([68d34ab](https://github.com/drivecore/mycoder/commit/68d34abf8a73ed533a072359ce334a9364753425)) +* **openai:** add OpenAI dependency to agent package and enable provider in config ([30b0807](https://github.com/drivecore/mycoder/commit/30b0807d4f3ecdd24f53b7ee4160645a4ed10444)) +* replace @semantic-release/npm with @anolilab/semantic-release-pnpm to properly resolve workspace references ([bacb51f](https://github.com/drivecore/mycoder/commit/bacb51f637f2b2d3b1039bdfdbd33e3d704b6cde)) +* up subagent iterations to 200 from 50 ([b405f1e](https://github.com/drivecore/mycoder/commit/b405f1e6d62eb5304dc1aa6c0ff28dc49dc67dce)) + + +### Features + +* add agent tracking to background tools ([4a3bcc7](https://github.com/drivecore/mycoder/commit/4a3bcc72f27af5fdbeeb407a748d5ecf3b7faed5)) +* add Ollama configuration options ([d5c3a96](https://github.com/drivecore/mycoder/commit/d5c3a96ce9463c98504c2a346796400df36bf3b0)) +* **agent:** implement agentStart and agentMessage tools ([62f8df3](https://github.com/drivecore/mycoder/commit/62f8df3dd083e2838c97ce89112f390461550ee6)), closes [#111](https://github.com/drivecore/mycoder/issues/111) [#111](https://github.com/drivecore/mycoder/issues/111) +* allow textEditor to overwrite existing files with create command ([d1cde65](https://github.com/drivecore/mycoder/commit/d1cde65df65bfcca288a47f14eedf5ad5939ed37)), closes [#192](https://github.com/drivecore/mycoder/issues/192) +* implement background tool tracking (issue [#112](https://github.com/drivecore/mycoder/issues/112)) ([b5bb489](https://github.com/drivecore/mycoder/commit/b5bb48981791acda74ee46b93d2d85e27e93a538)) +* implement Ollama provider for LLM abstraction ([597211b](https://github.com/drivecore/mycoder/commit/597211b90e43c4d52969eb5994d393c15d85ec97)) +* **llm:** add OpenAI support to LLM abstraction ([7bda811](https://github.com/drivecore/mycoder/commit/7bda811658e15b8dd41135cd9b2b90e9ea925e15)) +* **refactor:** agent ([a2f59c2](https://github.com/drivecore/mycoder/commit/a2f59c2f51643a44d6e1ff0c16b319deb1adc3f2)) + +# mycoder-agent-v1.0.0 (2025-03-11) + +### Bug Fixes +- **monorepo:** implement semantic-release-monorepo for proper versioning of sub-packages ([96c6284](https://github.com/drivecore/mycoder/commit/96c62848fbc3a4c1c591f3fd6202486e6461c4f2)) +- only consider response empty if no text AND no tool calls ([#127](https://github.com/drivecore/mycoder/issues/127)) ([af20ec5](https://github.com/drivecore/mycoder/commit/af20ec54468afed49632306fe553b307ab3c4ba5)) +- Replace shell commands with Node.js APIs for cross-platform compatibility ([07b4c24](https://github.com/drivecore/mycoder/commit/07b4c24fa17d19c468a76404a367f6afc0005517)) +- token caching ([5972e59](https://github.com/drivecore/mycoder/commit/5972e59ab572040e564d1756ab8a5625215e14dc)) +- use maxTokens in generateTextProps ([bfb9da9](https://github.com/drivecore/mycoder/commit/bfb9da9804d61840344e93cc5bea809e8e16f2ec)) ### Features -* add back token tracking, system prompt caching. ([ddc04ab](https://github.com/drivecore/mycoder/commit/ddc04ab0778eb2f571897e825c8d8ba17651db09)) -* add showStdIn and showStdout options to shellMessage and shellStart ([aed1b9f](https://github.com/drivecore/mycoder/commit/aed1b9f6ba489da19f2170c136861a7c80ad6e33)), closes [#167](https://github.com/drivecore/mycoder/issues/167) -* add token caching. issue 145 ([d78723b](https://github.com/drivecore/mycoder/commit/d78723bb6d0514110088caf7009e196e3f79769e)) -* remove modelProvider and modelName - instant decrepation ([59834dc](https://github.com/drivecore/mycoder/commit/59834dcf932051a5c75624bd6f6ab12254f43769)) +- add back token tracking, system prompt caching. ([ddc04ab](https://github.com/drivecore/mycoder/commit/ddc04ab0778eb2f571897e825c8d8ba17651db09)) +- add showStdIn and showStdout options to shellMessage and shellStart ([aed1b9f](https://github.com/drivecore/mycoder/commit/aed1b9f6ba489da19f2170c136861a7c80ad6e33)), closes [#167](https://github.com/drivecore/mycoder/issues/167) +- add token caching. issue 145 ([d78723b](https://github.com/drivecore/mycoder/commit/d78723bb6d0514110088caf7009e196e3f79769e)) +- remove modelProvider and modelName - instant decrepation ([59834dc](https://github.com/drivecore/mycoder/commit/59834dcf932051a5c75624bd6f6ab12254f43769)) # mycoder-agent diff --git a/packages/agent/package.json b/packages/agent/package.json index a979568..24baa66 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "mycoder-agent", - "version": "1.0.0", + "version": "1.1.0", "description": "Agent module for mycoder - an AI-powered software development assistant", "type": "module", "main": "dist/index.js", @@ -29,8 +29,7 @@ "typecheck": "tsc --noEmit", "clean": "rimraf dist", "clean:all": "rimraf node_modules dist", - "prepublishOnly": "pnpm run clean && pnpm run build && pnpm run test", - "semantic-release": "semantic-release -e semantic-release-monorepo" + "semantic-release": "pnpm exec semantic-release -e semantic-release-monorepo" }, "keywords": [ "ai", @@ -52,6 +51,7 @@ "chalk": "^5.4.1", "dotenv": "^16", "jsdom": "^26.0.0", + "openai": "^4.87.3", "playwright": "^1.50.1", "uuid": "^11", "zod": "^3.24.2", diff --git a/packages/agent/src/core/backgroundTools.test.ts b/packages/agent/src/core/backgroundTools.test.ts new file mode 100644 index 0000000..ec75544 --- /dev/null +++ b/packages/agent/src/core/backgroundTools.test.ts @@ -0,0 +1,185 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +import { + backgroundToolRegistry, + BackgroundToolStatus, + BackgroundToolType, +} from './backgroundTools.js'; + +// Mock uuid to return predictable IDs for testing +vi.mock('uuid', () => ({ + v4: vi.fn().mockReturnValue('test-id-1'), // Always return the same ID for simplicity in tests +})); + +describe('BackgroundToolRegistry', () => { + beforeEach(() => { + // Clear all registered tools before each test + const registry = backgroundToolRegistry as any; + registry.tools = new Map(); + }); + + it('should register a shell process', () => { + const id = backgroundToolRegistry.registerShell('agent-1', 'ls -la'); + + expect(id).toBe('test-id-1'); + + const tool = backgroundToolRegistry.getToolById(id); + expect(tool).toBeDefined(); + if (tool) { + expect(tool.type).toBe(BackgroundToolType.SHELL); + expect(tool.status).toBe(BackgroundToolStatus.RUNNING); + expect(tool.agentId).toBe('agent-1'); + if (tool.type === BackgroundToolType.SHELL) { + expect(tool.metadata.command).toBe('ls -la'); + } + } + }); + + it('should register a browser process', () => { + const id = backgroundToolRegistry.registerBrowser( + 'agent-1', + 'https://example.com', + ); + + expect(id).toBe('test-id-1'); + + const tool = backgroundToolRegistry.getToolById(id); + expect(tool).toBeDefined(); + if (tool) { + expect(tool.type).toBe(BackgroundToolType.BROWSER); + expect(tool.status).toBe(BackgroundToolStatus.RUNNING); + expect(tool.agentId).toBe('agent-1'); + if (tool.type === BackgroundToolType.BROWSER) { + expect(tool.metadata.url).toBe('https://example.com'); + } + } + }); + + it('should update tool status', () => { + const id = backgroundToolRegistry.registerShell('agent-1', 'sleep 10'); + + const updated = backgroundToolRegistry.updateToolStatus( + id, + BackgroundToolStatus.COMPLETED, + { + exitCode: 0, + }, + ); + + expect(updated).toBe(true); + + const tool = backgroundToolRegistry.getToolById(id); + expect(tool).toBeDefined(); + if (tool) { + expect(tool.status).toBe(BackgroundToolStatus.COMPLETED); + expect(tool.endTime).toBeDefined(); + if (tool.type === BackgroundToolType.SHELL) { + expect(tool.metadata.exitCode).toBe(0); + } + } + }); + + it('should return false when updating non-existent tool', () => { + const updated = backgroundToolRegistry.updateToolStatus( + 'non-existent-id', + BackgroundToolStatus.COMPLETED, + ); + + expect(updated).toBe(false); + }); + + it('should get tools by agent ID', () => { + // For this test, we'll directly manipulate the tools map + const registry = backgroundToolRegistry as any; + registry.tools = new Map(); + + // Add tools directly to the map with different agent IDs + registry.tools.set('id1', { + id: 'id1', + type: BackgroundToolType.SHELL, + status: BackgroundToolStatus.RUNNING, + startTime: new Date(), + agentId: 'agent-1', + metadata: { command: 'ls -la' }, + }); + + registry.tools.set('id2', { + id: 'id2', + type: BackgroundToolType.BROWSER, + status: BackgroundToolStatus.RUNNING, + startTime: new Date(), + agentId: 'agent-1', + metadata: { url: 'https://example.com' }, + }); + + registry.tools.set('id3', { + id: 'id3', + type: BackgroundToolType.SHELL, + status: BackgroundToolStatus.RUNNING, + startTime: new Date(), + agentId: 'agent-2', + metadata: { command: 'echo hello' }, + }); + + const agent1Tools = backgroundToolRegistry.getToolsByAgent('agent-1'); + const agent2Tools = backgroundToolRegistry.getToolsByAgent('agent-2'); + + expect(agent1Tools.length).toBe(2); + expect(agent2Tools.length).toBe(1); + }); + + it('should clean up old completed tools', () => { + // Create tools with specific dates + const registry = backgroundToolRegistry as any; + + // Add a completed tool from 25 hours ago + const oldTool = { + id: 'old-tool', + type: BackgroundToolType.SHELL, + status: BackgroundToolStatus.COMPLETED, + startTime: new Date(Date.now() - 25 * 60 * 60 * 1000), + endTime: new Date(Date.now() - 25 * 60 * 60 * 1000), + agentId: 'agent-1', + metadata: { command: 'echo old' }, + }; + + // Add a completed tool from 10 hours ago + const recentTool = { + id: 'recent-tool', + type: BackgroundToolType.SHELL, + status: BackgroundToolStatus.COMPLETED, + startTime: new Date(Date.now() - 10 * 60 * 60 * 1000), + endTime: new Date(Date.now() - 10 * 60 * 60 * 1000), + agentId: 'agent-1', + metadata: { command: 'echo recent' }, + }; + + // Add a running tool from 25 hours ago + const oldRunningTool = { + id: 'old-running-tool', + type: BackgroundToolType.SHELL, + status: BackgroundToolStatus.RUNNING, + startTime: new Date(Date.now() - 25 * 60 * 60 * 1000), + agentId: 'agent-1', + metadata: { command: 'sleep 100' }, + }; + + registry.tools.set('old-tool', oldTool); + registry.tools.set('recent-tool', recentTool); + registry.tools.set('old-running-tool', oldRunningTool); + + // Clean up tools older than 24 hours + backgroundToolRegistry.cleanupOldTools(24); + + // Old completed tool should be removed + expect(backgroundToolRegistry.getToolById('old-tool')).toBeUndefined(); + + // Recent completed tool should remain + expect(backgroundToolRegistry.getToolById('recent-tool')).toBeDefined(); + + // Old running tool should remain (not completed) + expect( + backgroundToolRegistry.getToolById('old-running-tool'), + ).toBeDefined(); + }); +}); diff --git a/packages/agent/src/core/backgroundTools.ts b/packages/agent/src/core/backgroundTools.ts new file mode 100644 index 0000000..ebe850b --- /dev/null +++ b/packages/agent/src/core/backgroundTools.ts @@ -0,0 +1,194 @@ +import { v4 as uuidv4 } from 'uuid'; + +// Types of background processes we can track +export enum BackgroundToolType { + SHELL = 'shell', + BROWSER = 'browser', + AGENT = 'agent', +} + +// Status of a background process +export enum BackgroundToolStatus { + RUNNING = 'running', + COMPLETED = 'completed', + ERROR = 'error', + TERMINATED = 'terminated', +} + +// Common interface for all background processes +export interface BackgroundTool { + id: string; + type: BackgroundToolType; + status: BackgroundToolStatus; + startTime: Date; + endTime?: Date; + agentId: string; // To track which agent created this process + metadata: Record; // Additional tool-specific information +} + +// Shell process specific data +export interface ShellBackgroundTool extends BackgroundTool { + type: BackgroundToolType.SHELL; + metadata: { + command: string; + exitCode?: number | null; + signaled?: boolean; + }; +} + +// Browser process specific data +export interface BrowserBackgroundTool extends BackgroundTool { + type: BackgroundToolType.BROWSER; + metadata: { + url?: string; + }; +} + +// Agent process specific data (for future use) +export interface AgentBackgroundTool extends BackgroundTool { + type: BackgroundToolType.AGENT; + metadata: { + goal?: string; + }; +} + +// Utility type for all background tool types +export type AnyBackgroundTool = + | ShellBackgroundTool + | BrowserBackgroundTool + | AgentBackgroundTool; + +/** + * Registry to keep track of all background processes + */ +export class BackgroundToolRegistry { + private static instance: BackgroundToolRegistry; + private tools: Map = new Map(); + + // Private constructor for singleton pattern + private constructor() {} + + // Get the singleton instance + public static getInstance(): BackgroundToolRegistry { + if (!BackgroundToolRegistry.instance) { + BackgroundToolRegistry.instance = new BackgroundToolRegistry(); + } + return BackgroundToolRegistry.instance; + } + + // Register a new shell process + public registerShell(agentId: string, command: string): string { + const id = uuidv4(); + const tool: ShellBackgroundTool = { + id, + type: BackgroundToolType.SHELL, + status: BackgroundToolStatus.RUNNING, + startTime: new Date(), + agentId, + metadata: { + command, + }, + }; + this.tools.set(id, tool); + return id; + } + + // Register a new browser process + public registerBrowser(agentId: string, url?: string): string { + const id = uuidv4(); + const tool: BrowserBackgroundTool = { + id, + type: BackgroundToolType.BROWSER, + status: BackgroundToolStatus.RUNNING, + startTime: new Date(), + agentId, + metadata: { + url, + }, + }; + this.tools.set(id, tool); + return id; + } + + // Register a new agent process (for future use) + public registerAgent(agentId: string, goal?: string): string { + const id = uuidv4(); + const tool: AgentBackgroundTool = { + id, + type: BackgroundToolType.AGENT, + status: BackgroundToolStatus.RUNNING, + startTime: new Date(), + agentId, + metadata: { + goal, + }, + }; + this.tools.set(id, tool); + return id; + } + + // Update the status of a process + public updateToolStatus( + id: string, + status: BackgroundToolStatus, + metadata?: Record, + ): boolean { + const tool = this.tools.get(id); + if (!tool) { + return false; + } + + tool.status = status; + + if ( + status === BackgroundToolStatus.COMPLETED || + status === BackgroundToolStatus.ERROR || + status === BackgroundToolStatus.TERMINATED + ) { + tool.endTime = new Date(); + } + + if (metadata) { + tool.metadata = { ...tool.metadata, ...metadata }; + } + + return true; + } + + // Get all processes for a specific agent + public getToolsByAgent(agentId: string): AnyBackgroundTool[] { + const result: AnyBackgroundTool[] = []; + for (const tool of this.tools.values()) { + if (tool.agentId === agentId) { + result.push(tool); + } + } + return result; + } + + // Get a specific process by ID + public getToolById(id: string): AnyBackgroundTool | undefined { + return this.tools.get(id); + } + + // Clean up completed processes (optional, for maintenance) + public cleanupOldTools(olderThanHours: number = 24): void { + const cutoffTime = new Date(Date.now() - olderThanHours * 60 * 60 * 1000); + + for (const [id, tool] of this.tools.entries()) { + // Remove if it's completed/error/terminated AND older than cutoff + if ( + tool.endTime && + tool.endTime < cutoffTime && + (tool.status === BackgroundToolStatus.COMPLETED || + tool.status === BackgroundToolStatus.ERROR || + tool.status === BackgroundToolStatus.TERMINATED) + ) { + this.tools.delete(id); + } + } + } +} + +// Export singleton instance +export const backgroundToolRegistry = BackgroundToolRegistry.getInstance(); diff --git a/packages/agent/src/core/executeToolCall.ts b/packages/agent/src/core/executeToolCall.ts index 8a688c4..e44c62c 100644 --- a/packages/agent/src/core/executeToolCall.ts +++ b/packages/agent/src/core/executeToolCall.ts @@ -23,10 +23,7 @@ export const executeToolCall = async ( customPrefix: tool.logPrefix, }); - const toolContext: ToolContext = { - ...context, - logger, - }; + const toolContext: ToolContext = { ...context, logger }; let parsedJson: any; try { diff --git a/packages/agent/src/core/llm/__tests__/openai.test.ts b/packages/agent/src/core/llm/__tests__/openai.test.ts new file mode 100644 index 0000000..2eaf476 --- /dev/null +++ b/packages/agent/src/core/llm/__tests__/openai.test.ts @@ -0,0 +1,221 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +import { TokenUsage } from '../../tokens.js'; +import { OpenAIProvider } from '../providers/openai.js'; + +// Mock the OpenAI module +vi.mock('openai', () => { + // Create a mock function for the create method + const mockCreate = vi.fn().mockResolvedValue({ + id: 'chatcmpl-123', + object: 'chat.completion', + created: 1677858242, + model: 'gpt-4', + choices: [ + { + index: 0, + message: { + role: 'assistant', + content: 'This is a test response', + tool_calls: [ + { + id: 'tool-call-1', + type: 'function', + function: { + name: 'testFunction', + arguments: '{"arg1":"value1"}', + }, + }, + ], + }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 10, + completion_tokens: 20, + total_tokens: 30, + }, + }); + + // Return a mocked version of the OpenAI class + return { + default: class MockOpenAI { + constructor() { + // Constructor implementation + } + + chat = { + completions: { + create: mockCreate, + }, + }; + }, + }; +}); + +describe('OpenAIProvider', () => { + let provider: OpenAIProvider; + + beforeEach(() => { + // Set environment variable for testing + process.env.OPENAI_API_KEY = 'test-api-key'; + provider = new OpenAIProvider('gpt-4'); + }); + + it('should initialize with correct properties', () => { + expect(provider.name).toBe('openai'); + expect(provider.provider).toBe('openai.chat'); + expect(provider.model).toBe('gpt-4'); + }); + + it('should throw error if API key is missing', () => { + // Clear environment variable + const originalKey = process.env.OPENAI_API_KEY; + delete process.env.OPENAI_API_KEY; + + expect(() => new OpenAIProvider('gpt-4')).toThrow( + 'OpenAI API key is required', + ); + + // Restore environment variable + process.env.OPENAI_API_KEY = originalKey; + }); + + it('should generate text and handle tool calls', async () => { + const response = await provider.generateText({ + messages: [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: 'Hello, can you help me?' }, + ], + functions: [ + { + name: 'testFunction', + description: 'A test function', + parameters: { + type: 'object', + properties: { + arg1: { type: 'string' }, + }, + }, + }, + ], + }); + + expect(response.text).toBe('This is a test response'); + expect(response.toolCalls).toHaveLength(1); + + const toolCall = response.toolCalls[0]; + expect(toolCall).toBeDefined(); + expect(toolCall?.name).toBe('testFunction'); + expect(toolCall?.id).toBe('tool-call-1'); + expect(toolCall?.content).toBe('{"arg1":"value1"}'); + + // Check token usage + expect(response.tokenUsage).toBeInstanceOf(TokenUsage); + expect(response.tokenUsage.input).toBe(10); + expect(response.tokenUsage.output).toBe(20); + }); + + it('should format messages correctly', async () => { + await provider.generateText({ + messages: [ + { role: 'system', content: 'You are a helpful assistant.' }, + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there' }, + { + role: 'tool_use', + id: 'tool-1', + name: 'testTool', + content: '{"param":"value"}', + }, + { + role: 'tool_result', + tool_use_id: 'tool-1', + content: '{"result":"success"}', + is_error: false, + }, + ], + }); + + // Get the mock instance + const client = provider['client']; + const mockOpenAI = client?.chat?.completions + ?.create as unknown as ReturnType; + + // Check that messages were formatted correctly + expect(mockOpenAI).toHaveBeenCalled(); + + // Get the second call arguments (from this test) + const calledWith = mockOpenAI.mock.calls[1]?.[0] || {}; + + expect(calledWith.messages).toHaveLength(5); + + // We need to check each message individually to avoid TypeScript errors + const systemMessage = calledWith.messages[0]; + if ( + systemMessage && + typeof systemMessage === 'object' && + 'role' in systemMessage + ) { + expect(systemMessage.role).toBe('system'); + expect(systemMessage.content).toBe('You are a helpful assistant.'); + } + + const userMessage = calledWith.messages[1]; + if ( + userMessage && + typeof userMessage === 'object' && + 'role' in userMessage + ) { + expect(userMessage.role).toBe('user'); + expect(userMessage.content).toBe('Hello'); + } + + const assistantMessage = calledWith.messages[2]; + if ( + assistantMessage && + typeof assistantMessage === 'object' && + 'role' in assistantMessage + ) { + expect(assistantMessage.role).toBe('assistant'); + expect(assistantMessage.content).toBe('Hi there'); + } + + // Check tool_use formatting + const toolUseMessage = calledWith.messages[3]; + if ( + toolUseMessage && + typeof toolUseMessage === 'object' && + 'role' in toolUseMessage + ) { + expect(toolUseMessage.role).toBe('assistant'); + expect(toolUseMessage.content).toBe(null); + + if ( + 'tool_calls' in toolUseMessage && + Array.isArray(toolUseMessage.tool_calls) + ) { + expect(toolUseMessage.tool_calls.length).toBe(1); + const toolCall = toolUseMessage.tool_calls[0]; + if (toolCall && 'function' in toolCall) { + expect(toolCall.function.name).toBe('testTool'); + } + } + } + + // Check tool_result formatting + const toolResultMessage = calledWith.messages[4]; + if ( + toolResultMessage && + typeof toolResultMessage === 'object' && + 'role' in toolResultMessage + ) { + expect(toolResultMessage.role).toBe('tool'); + expect(toolResultMessage.content).toBe('{"result":"success"}'); + if ('tool_call_id' in toolResultMessage) { + expect(toolResultMessage.tool_call_id).toBe('tool-1'); + } + } + }); +}); diff --git a/packages/agent/src/core/llm/provider.ts b/packages/agent/src/core/llm/provider.ts index 379bbef..2bb2b29 100644 --- a/packages/agent/src/core/llm/provider.ts +++ b/packages/agent/src/core/llm/provider.ts @@ -3,6 +3,8 @@ */ import { AnthropicProvider } from './providers/anthropic.js'; +import { OllamaProvider } from './providers/ollama.js'; +import { OpenAIProvider } from './providers/openai.js'; import { ProviderOptions, GenerateOptions, LLMResponse } from './types.js'; /** @@ -39,6 +41,8 @@ const providerFactories: Record< (model: string, options: ProviderOptions) => LLMProvider > = { anthropic: (model, options) => new AnthropicProvider(model, options), + openai: (model, options) => new OpenAIProvider(model, options), + ollama: (model, options) => new OllamaProvider(model, options), }; /** diff --git a/packages/agent/src/core/llm/providers/ollama.ts b/packages/agent/src/core/llm/providers/ollama.ts new file mode 100644 index 0000000..c3a4869 --- /dev/null +++ b/packages/agent/src/core/llm/providers/ollama.ts @@ -0,0 +1,177 @@ +/** + * Ollama provider implementation + */ + +import { TokenUsage } from '../../tokens.js'; +import { LLMProvider } from '../provider.js'; +import { + GenerateOptions, + LLMResponse, + Message, + ProviderOptions, +} from '../types.js'; + +/** + * Ollama-specific options + */ +export interface OllamaOptions extends ProviderOptions { + baseUrl?: string; +} + +/** + * Ollama provider implementation + */ +export class OllamaProvider implements LLMProvider { + name: string = 'ollama'; + provider: string = 'ollama.chat'; + model: string; + private baseUrl: string; + + constructor(model: string, options: OllamaOptions = {}) { + this.model = model; + this.baseUrl = + options.baseUrl || + process.env.OLLAMA_BASE_URL || + 'http://localhost:11434'; + + // Ensure baseUrl doesn't end with a slash + if (this.baseUrl.endsWith('/')) { + this.baseUrl = this.baseUrl.slice(0, -1); + } + } + + /** + * Generate text using Ollama API + */ + async generateText(options: GenerateOptions): Promise { + const { + messages, + functions, + temperature = 0.7, + maxTokens, + topP, + frequencyPenalty, + presencePenalty, + } = options; + + // Format messages for Ollama API + const formattedMessages = this.formatMessages(messages); + + try { + // Prepare request options + const requestOptions: any = { + model: this.model, + messages: formattedMessages, + stream: false, + options: { + temperature: temperature, + // Ollama uses top_k instead of top_p, but we'll include top_p if provided + ...(topP !== undefined && { top_p: topP }), + ...(frequencyPenalty !== undefined && { + frequency_penalty: frequencyPenalty, + }), + ...(presencePenalty !== undefined && { + presence_penalty: presencePenalty, + }), + }, + }; + + // Add max_tokens if provided + if (maxTokens !== undefined) { + requestOptions.options.num_predict = maxTokens; + } + + // Add functions/tools if provided + if (functions && functions.length > 0) { + requestOptions.tools = functions.map((fn) => ({ + name: fn.name, + description: fn.description, + parameters: fn.parameters, + })); + } + + // Make the API request + const response = await fetch(`${this.baseUrl}/api/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestOptions), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Ollama API error: ${response.status} ${errorText}`); + } + + const data = await response.json(); + + // Extract content and tool calls + const content = data.message?.content || ''; + const toolCalls = + data.message?.tool_calls?.map((toolCall: any) => ({ + id: + toolCall.id || + `tool-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`, + name: toolCall.name, + content: JSON.stringify(toolCall.args || toolCall.arguments || {}), + })) || []; + + // Create token usage from response data + const tokenUsage = new TokenUsage(); + tokenUsage.input = data.prompt_eval_count || 0; + tokenUsage.output = data.eval_count || 0; + + return { + text: content, + toolCalls: toolCalls, + tokenUsage: tokenUsage, + }; + } catch (error) { + throw new Error(`Error calling Ollama API: ${(error as Error).message}`); + } + } + + /** + * Format messages for Ollama API + */ + private formatMessages(messages: Message[]): any[] { + return messages.map((msg) => { + if ( + msg.role === 'user' || + msg.role === 'assistant' || + msg.role === 'system' + ) { + return { + role: msg.role, + content: msg.content, + }; + } else if (msg.role === 'tool_result') { + // Ollama expects tool results as a 'tool' role + return { + role: 'tool', + content: msg.content, + tool_call_id: msg.tool_use_id, + }; + } else if (msg.role === 'tool_use') { + // We'll convert tool_use to assistant messages with tool_calls + return { + role: 'assistant', + content: '', + tool_calls: [ + { + id: msg.id, + name: msg.name, + arguments: msg.content, + }, + ], + }; + } + // Default fallback for unknown message types + return { + role: 'user', + content: (msg as any).content || '', + }; + }); + } +} diff --git a/packages/agent/src/core/llm/providers/openai.ts b/packages/agent/src/core/llm/providers/openai.ts new file mode 100644 index 0000000..676f8a8 --- /dev/null +++ b/packages/agent/src/core/llm/providers/openai.ts @@ -0,0 +1,207 @@ +/** + * OpenAI provider implementation + */ +import OpenAI from 'openai'; + +import { TokenUsage } from '../../tokens.js'; +import { ToolCall } from '../../types'; +import { LLMProvider } from '../provider.js'; +import { + GenerateOptions, + LLMResponse, + Message, + ProviderOptions, + FunctionDefinition, +} from '../types.js'; + +import type { + ChatCompletionMessageParam, + ChatCompletionTool, +} from 'openai/resources/chat'; + +/** + * OpenAI-specific options + */ +export interface OpenAIOptions extends ProviderOptions { + apiKey?: string; + baseUrl?: string; + organization?: string; +} + +/** + * OpenAI provider implementation + */ +export class OpenAIProvider implements LLMProvider { + name: string = 'openai'; + provider: string = 'openai.chat'; + model: string; + private client: OpenAI; + private apiKey: string; + private baseUrl?: string; + private organization?: string; + + constructor(model: string, options: OpenAIOptions = {}) { + this.model = model; + this.apiKey = options.apiKey || process.env.OPENAI_API_KEY || ''; + this.baseUrl = options.baseUrl; + this.organization = options.organization || process.env.OPENAI_ORGANIZATION; + + if (!this.apiKey) { + throw new Error('OpenAI API key is required'); + } + + // Initialize OpenAI client + this.client = new OpenAI({ + apiKey: this.apiKey, + ...(this.baseUrl && { baseURL: this.baseUrl }), + ...(this.organization && { organization: this.organization }), + }); + } + + /** + * Generate text using OpenAI API + */ + async generateText(options: GenerateOptions): Promise { + const { + messages, + functions, + temperature = 0.7, + maxTokens, + stopSequences, + topP, + presencePenalty, + frequencyPenalty, + responseFormat, + } = options; + + // Format messages for OpenAI + const formattedMessages = this.formatMessages(messages); + + // Format functions for OpenAI + const tools = functions ? this.formatFunctions(functions) : undefined; + + try { + const requestOptions = { + model: this.model, + messages: formattedMessages, + temperature, + max_tokens: maxTokens, + stop: stopSequences, + top_p: topP, + presence_penalty: presencePenalty, + frequency_penalty: frequencyPenalty, + tools: tools, + response_format: + responseFormat === 'json_object' + ? { type: 'json_object' as const } + : undefined, + }; + + const response = + await this.client.chat.completions.create(requestOptions); + + // Extract content and tool calls + const message = response.choices[0]?.message; + const content = message?.content || ''; + + // Handle tool calls if present + const toolCalls: ToolCall[] = []; + if (message?.tool_calls) { + for (const tool of message.tool_calls) { + if (tool.type === 'function') { + toolCalls.push({ + id: tool.id, + name: tool.function.name, + content: tool.function.arguments, + }); + } + } + } + + // Create token usage + const tokenUsage = new TokenUsage(); + tokenUsage.input = response.usage?.prompt_tokens || 0; + tokenUsage.output = response.usage?.completion_tokens || 0; + + return { + text: content, + toolCalls, + tokenUsage, + }; + } catch (error) { + throw new Error(`Error calling OpenAI API: ${(error as Error).message}`); + } + } + + /** + * Format messages for OpenAI API + */ + private formatMessages(messages: Message[]): ChatCompletionMessageParam[] { + return messages.map((msg): ChatCompletionMessageParam => { + // Use switch for better type narrowing + switch (msg.role) { + case 'user': + return { + role: 'user', + content: msg.content, + }; + case 'system': + return { + role: 'system', + content: msg.content, + }; + case 'assistant': + return { + role: 'assistant', + content: msg.content, + }; + case 'tool_use': + // OpenAI doesn't have a direct equivalent to tool_use, + // so we'll include it as a function call in an assistant message + return { + role: 'assistant', + content: null, + tool_calls: [ + { + id: msg.id, + type: 'function' as const, + function: { + name: msg.name, + arguments: msg.content, + }, + }, + ], + }; + case 'tool_result': + // Tool results in OpenAI are represented as tool messages + return { + role: 'tool', + content: msg.content, + tool_call_id: msg.tool_use_id, + }; + default: + // For any other role, default to user message + return { + role: 'user', + content: 'Unknown message type', + }; + } + }); + } + + /** + * Format functions for OpenAI API + */ + private formatFunctions( + functions: FunctionDefinition[], + ): ChatCompletionTool[] { + return functions.map((fn) => ({ + type: 'function' as const, + function: { + name: fn.name, + description: fn.description, + parameters: fn.parameters, + }, + })); + } +} diff --git a/packages/agent/src/core/toolAgent.test.ts b/packages/agent/src/core/toolAgent.test.ts index 91226dc..5bf787b 100644 --- a/packages/agent/src/core/toolAgent.test.ts +++ b/packages/agent/src/core/toolAgent.test.ts @@ -1,22 +1,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { z } from 'zod'; -import { MockLogger } from '../utils/mockLogger.js'; +import { getMockToolContext } from '../tools/getTools.test.js'; import { executeToolCall } from './executeToolCall.js'; -import { TokenTracker } from './tokens.js'; import { Tool, ToolContext } from './types.js'; -const toolContext: ToolContext = { - logger: new MockLogger(), - headless: true, - workingDirectory: '.', - userSession: false, - pageFilter: 'simple', - tokenTracker: new TokenTracker(), - githubMode: false, -}; - +const toolContext: ToolContext = getMockToolContext(); // Mock tool for testing const mockTool: Tool = { name: 'mockTool', diff --git a/packages/agent/src/core/toolAgent/config.test.ts b/packages/agent/src/core/toolAgent/config.test.ts index 8c37501..1bb5951 100644 --- a/packages/agent/src/core/toolAgent/config.test.ts +++ b/packages/agent/src/core/toolAgent/config.test.ts @@ -1,45 +1,62 @@ import { describe, expect, it } from 'vitest'; -import { getModel } from './config'; +import { createProvider } from '../llm/provider.js'; -describe('getModel', () => { +import { getModel } from './config.js'; + +describe('createProvider', () => { it('should return the correct model for anthropic', () => { - const model = getModel('anthropic', 'claude-3-7-sonnet-20250219'); + const model = createProvider('anthropic', 'claude-3-7-sonnet-20250219', { + apiKey: 'sk-proj-1234567890', + }); expect(model).toBeDefined(); expect(model.provider).toBe('anthropic.messages'); }); - /* - it('should return the correct model for openai', () => { - const model = getModel('openai', 'gpt-4o-2024-05-13'); + const model = createProvider('openai', 'gpt-4o-2024-05-13', { + apiKey: 'sk-proj-1234567890', + }); expect(model).toBeDefined(); expect(model.provider).toBe('openai.chat'); }); - it('should return the correct model for ollama', () => { - const model = getModel('ollama', 'llama3'); + const model = createProvider('ollama', 'llama3'); + expect(model).toBeDefined(); + expect(model.provider).toBe('ollama.chat'); + }); + + it('should return the correct model for ollama with custom base URL', () => { + const model = getModel('ollama', 'llama3', { + ollamaBaseUrl: 'http://custom-ollama:11434', + }); expect(model).toBeDefined(); expect(model.provider).toBe('ollama.chat'); }); + /* + it('should return the correct model for openai', () => { + const model = getModel('openai', 'gpt-4o-2024-05-13'); + expect(model).toBeDefined(); + expect(model.provider).toBe('openai.chat'); + }); + it('should return the correct model for xai', () => { - const model = getModel('xai', 'grok-1'); + const model = createProvider('xai', 'grok-1'); expect(model).toBeDefined(); expect(model.provider).toBe('xai.chat'); }); it('should return the correct model for mistral', () => { - const model = getModel('mistral', 'mistral-large-latest'); + const model = createProvider('mistral', 'mistral-large-latest'); expect(model).toBeDefined(); expect(model.provider).toBe('mistral.chat'); }); -*/ + */ it('should throw an error for unknown provider', () => { expect(() => { - // @ts-expect-error Testing invalid provider - getModel('unknown', 'model'); - }).toThrow('Unknown model provider: unknown'); + createProvider('unknown', 'model'); + }).toThrow(); }); }); diff --git a/packages/agent/src/core/toolAgent/config.ts b/packages/agent/src/core/toolAgent/config.ts index 29737c9..fea22e8 100644 --- a/packages/agent/src/core/toolAgent/config.ts +++ b/packages/agent/src/core/toolAgent/config.ts @@ -8,21 +8,28 @@ import { ToolContext } from '../types'; /** * Available model providers */ -export type ModelProvider = 'anthropic'; +export type ModelProvider = 'anthropic' | 'openai' | 'ollama'; /* - | 'openai' - | 'ollama' | 'xai' | 'mistral'*/ +export type AgentConfig = { + maxIterations: number; + getSystemPrompt: (toolContext: ToolContext) => string; +}; + /** * Get the model instance based on provider and model name */ -export function getModel(provider: ModelProvider, model: string): LLMProvider { +export function getModel( + provider: ModelProvider, + model: string, + options?: { ollamaBaseUrl?: string }, +): LLMProvider { switch (provider) { case 'anthropic': return createProvider('anthropic', model); - /*case 'openai': + case 'openai': return createProvider('openai', model); case 'ollama': if (options?.ollamaBaseUrl) { @@ -31,7 +38,7 @@ export function getModel(provider: ModelProvider, model: string): LLMProvider { }); } return createProvider('ollama', model); - case 'xai': + /*case 'xai': return createProvider('xai', model); case 'mistral': return createProvider('mistral', model);*/ @@ -39,15 +46,11 @@ export function getModel(provider: ModelProvider, model: string): LLMProvider { throw new Error(`Unknown model provider: ${provider}`); } } - /** * Default configuration for the tool agent */ -export const DEFAULT_CONFIG = { +export const DEFAULT_CONFIG: AgentConfig = { maxIterations: 200, - model: getModel('anthropic', 'claude-3-7-sonnet-20250219'), - maxTokens: 4096, - temperature: 0.7, getSystemPrompt: getDefaultSystemPrompt, }; diff --git a/packages/agent/src/core/toolAgent/toolAgentCore.ts b/packages/agent/src/core/toolAgent/toolAgentCore.ts index 0ac4be7..da00326 100644 --- a/packages/agent/src/core/toolAgent/toolAgentCore.ts +++ b/packages/agent/src/core/toolAgent/toolAgentCore.ts @@ -1,12 +1,14 @@ import { zodToJsonSchema } from 'zod-to-json-schema'; import { generateText } from '../llm/core.js'; +import { createProvider } from '../llm/provider.js'; import { Message, ToolUseMessage } from '../llm/types.js'; +import { Tool, ToolContext } from '../types.js'; -import { DEFAULT_CONFIG } from './config.js'; +import { AgentConfig } from './config.js'; import { logTokenUsage } from './tokenTracking.js'; import { executeTools } from './toolExecutor.js'; -import { Tool, ToolAgentResult, ToolContext } from './types.js'; +import { ToolAgentResult } from './types.js'; // Import from our new LLM abstraction instead of Vercel AI SDK @@ -17,7 +19,7 @@ import { Tool, ToolAgentResult, ToolContext } from './types.js'; export const toolAgent = async ( initialPrompt: string, tools: Tool[], - config = DEFAULT_CONFIG, + config: AgentConfig, context: ToolContext, ): Promise => { const { logger, tokenTracker } = context; @@ -41,7 +43,7 @@ export const toolAgent = async ( const systemPrompt = config.getSystemPrompt(context); // Create the LLM provider - const provider = config.model; + const provider = createProvider(context.provider, context.model); for (let i = 0; i < config.maxIterations; i++) { logger.verbose( @@ -72,8 +74,8 @@ export const toolAgent = async ( const generateOptions = { messages: messagesWithSystem, functions: functionDefinitions, - temperature: config.temperature, - maxTokens: config.maxTokens, + temperature: context.temperature, + maxTokens: context.maxTokens, }; const { text, toolCalls, tokenUsage } = await generateText( diff --git a/packages/agent/src/core/toolAgent/toolExecutor.ts b/packages/agent/src/core/toolAgent/toolExecutor.ts index 7c82543..9d18258 100644 --- a/packages/agent/src/core/toolAgent/toolExecutor.ts +++ b/packages/agent/src/core/toolAgent/toolExecutor.ts @@ -1,10 +1,10 @@ import { executeToolCall } from '../executeToolCall.js'; import { Message } from '../llm/types.js'; import { TokenTracker } from '../tokens.js'; -import { ToolCall } from '../types.js'; +import { Tool, ToolCall, ToolContext } from '../types.js'; import { addToolResultToMessages } from './messageUtils.js'; -import { Tool, ToolCallResult, ToolContext } from './types.js'; +import { ToolCallResult } from './types.js'; const safeParse = (value: string) => { try { diff --git a/packages/agent/src/core/toolAgent/types.ts b/packages/agent/src/core/toolAgent/types.ts index 52c6b12..62588f4 100644 --- a/packages/agent/src/core/toolAgent/types.ts +++ b/packages/agent/src/core/toolAgent/types.ts @@ -1,8 +1,4 @@ // Import types from the core types file -import { Tool, ToolContext } from '../types.js'; - -// Export the imported types explicitly -export { Tool, ToolContext }; // Define types specific to toolAgent here export interface ToolAgentResult { diff --git a/packages/agent/src/core/types.ts b/packages/agent/src/core/types.ts index 738a79b..59c70d0 100644 --- a/packages/agent/src/core/types.ts +++ b/packages/agent/src/core/types.ts @@ -4,6 +4,7 @@ import { JsonSchema7Type } from 'zod-to-json-schema'; import { Logger } from '../utils/logger.js'; import { TokenTracker } from './tokens.js'; +import { ModelProvider } from './toolAgent/config.js'; export type TokenLevel = 'debug' | 'verbose' | 'info' | 'warn' | 'error'; @@ -19,7 +20,12 @@ export type ToolContext = { githubMode: boolean; customPrompt?: string; tokenCache?: boolean; - enableUserPrompt?: boolean; + userPrompt?: boolean; + agentId?: string; // Unique identifier for the agent, used for background tool tracking + provider: ModelProvider; + model: string; + maxTokens: number; + temperature: number; }; export type Tool, TReturn = any> = { diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index a816669..fdf802d 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -9,6 +9,7 @@ export * from './tools/system/respawn.js'; export * from './tools/system/sequenceComplete.js'; export * from './tools/system/shellMessage.js'; export * from './tools/system/shellExecute.js'; +export * from './tools/system/listBackgroundTools.js'; // Tools - Browser export * from './tools/browser/BrowserManager.js'; @@ -25,6 +26,7 @@ export * from './tools/interaction/userPrompt.js'; // Core export * from './core/executeToolCall.js'; export * from './core/types.js'; +export * from './core/backgroundTools.js'; // Tool Agent Core export { toolAgent } from './core/toolAgent/toolAgentCore.js'; export * from './core/toolAgent/config.js'; @@ -32,6 +34,7 @@ export * from './core/toolAgent/messageUtils.js'; export * from './core/toolAgent/toolExecutor.js'; export * from './core/toolAgent/tokenTracking.js'; export * from './core/toolAgent/types.js'; +export * from './core/llm/provider.js'; // Utils export * from './tools/getTools.js'; diff --git a/packages/agent/src/tools/browser/BrowserManager.ts b/packages/agent/src/tools/browser/BrowserManager.ts index a136e8a..269597a 100644 --- a/packages/agent/src/tools/browser/BrowserManager.ts +++ b/packages/agent/src/tools/browser/BrowserManager.ts @@ -15,6 +15,15 @@ export class BrowserManager { defaultTimeout: 30000, }; + constructor() { + // Store a reference to the instance globally for cleanup + // This allows the CLI to access the instance for cleanup + (globalThis as any).__BROWSER_MANAGER__ = this; + + // Set up cleanup handlers for graceful shutdown + this.setupGlobalCleanup(); + } + async createSession(config?: BrowserConfig): Promise { try { const sessionConfig = { ...this.defaultConfig, ...config }; @@ -80,14 +89,46 @@ export class BrowserManager { this.sessions.delete(session.id); }); - // Handle process exit + // No need to add individual process handlers for each session + // We'll handle all sessions in the global cleanup + } + + /** + * Sets up global cleanup handlers for all browser sessions + */ + private setupGlobalCleanup(): void { + // Use beforeExit for async cleanup + process.on('beforeExit', () => { + this.closeAllSessions().catch((err) => { + console.error('Error closing browser sessions:', err); + }); + }); + + // Use exit for synchronous cleanup (as a fallback) process.on('exit', () => { - this.closeSession(session.id).catch(() => {}); + // Can only do synchronous operations here + for (const session of this.sessions.values()) { + try { + // Attempt synchronous close - may not fully work + session.browser.close(); + // eslint-disable-next-line unused-imports/no-unused-vars + } catch (e) { + // Ignore errors during exit + } + } }); - // Handle unexpected errors - process.on('uncaughtException', () => { - this.closeSession(session.id).catch(() => {}); + // Handle SIGINT (Ctrl+C) + process.on('SIGINT', () => { + // eslint-disable-next-line promise/catch-or-return + this.closeAllSessions() + .catch(() => { + return false; + }) + .finally(() => { + // Give a moment for cleanup to complete + setTimeout(() => process.exit(0), 500); + }); }); } diff --git a/packages/agent/src/tools/browser/browseMessage.ts b/packages/agent/src/tools/browser/browseMessage.ts index 5643321..abe07c3 100644 --- a/packages/agent/src/tools/browser/browseMessage.ts +++ b/packages/agent/src/tools/browser/browseMessage.ts @@ -1,6 +1,10 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; +import { + backgroundToolRegistry, + BackgroundToolStatus, +} from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; import { errorToString } from '../../utils/errorToString.js'; import { sleep } from '../../utils/sleep.js'; @@ -184,6 +188,16 @@ export const browseMessageTool: Tool = { await session.page.context().close(); await session.browser.close(); browserSessions.delete(instanceId); + + // Update background tool registry when browser is explicitly closed + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.COMPLETED, + { + closedExplicitly: true, + }, + ); + logger.verbose('Browser session closed successfully'); return { status: 'closed' }; } @@ -194,6 +208,17 @@ export const browseMessageTool: Tool = { } } catch (error) { logger.error('Browser action failed:', { error }); + + // Update background tool registry with error status if action fails + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.ERROR, + { + error: errorToString(error), + actionType, + }, + ); + return { status: 'error', error: errorToString(error), diff --git a/packages/agent/src/tools/browser/browseStart.ts b/packages/agent/src/tools/browser/browseStart.ts index 8ba5a65..8d95000 100644 --- a/packages/agent/src/tools/browser/browseStart.ts +++ b/packages/agent/src/tools/browser/browseStart.ts @@ -3,6 +3,10 @@ import { v4 as uuidv4 } from 'uuid'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; +import { + backgroundToolRegistry, + BackgroundToolStatus, +} from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; import { errorToString } from '../../utils/errorToString.js'; import { sleep } from '../../utils/sleep.js'; @@ -42,7 +46,7 @@ export const browseStartTool: Tool = { execute: async ( { url, timeout = 30000 }, - { logger, headless, userSession, pageFilter }, + { logger, headless, userSession, pageFilter, agentId }, ): Promise => { logger.verbose(`Starting browser session${url ? ` at ${url}` : ''}`); logger.verbose( @@ -53,6 +57,9 @@ export const browseStartTool: Tool = { try { const instanceId = uuidv4(); + // Register this browser session with the background tool registry + backgroundToolRegistry.registerBrowser(agentId || 'unknown', url); + // Launch browser const launchOptions = { headless, @@ -91,6 +98,11 @@ export const browseStartTool: Tool = { // Setup cleanup handlers browser.on('disconnected', () => { browserSessions.delete(instanceId); + // Update background tool registry when browser disconnects + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.TERMINATED, + ); }); // Navigate to URL if provided @@ -133,6 +145,16 @@ export const browseStartTool: Tool = { logger.verbose('Browser session started successfully'); logger.verbose(`Content length: ${content.length} characters`); + // Update background tool registry with running status + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.RUNNING, + { + url: url || 'about:blank', + contentLength: content.length, + }, + ); + return { instanceId, status: 'initialized', @@ -140,6 +162,10 @@ export const browseStartTool: Tool = { }; } catch (error) { logger.error(`Failed to start browser: ${errorToString(error)}`); + + // No need to update background tool registry here as we don't have a valid instanceId + // when an error occurs before the browser is properly initialized + return { instanceId: '', status: 'error', diff --git a/packages/agent/src/tools/getTools.test.ts b/packages/agent/src/tools/getTools.test.ts index 2023b55..45965fe 100644 --- a/packages/agent/src/tools/getTools.test.ts +++ b/packages/agent/src/tools/getTools.test.ts @@ -1,7 +1,26 @@ import { describe, it, expect } from 'vitest'; +import { TokenTracker } from '../core/tokens.js'; +import { ToolContext } from '../core/types.js'; +import { MockLogger } from '../utils/mockLogger.js'; + import { getTools } from './getTools.js'; +// Mock context +export const getMockToolContext = (): ToolContext => ({ + logger: new MockLogger(), + tokenTracker: new TokenTracker(), + workingDirectory: '.', + headless: true, + userSession: false, + pageFilter: 'none', + githubMode: true, + provider: 'anthropic', + model: 'claude-3-7-sonnet-20250219', + maxTokens: 4096, + temperature: 0.7, +}); + describe('getTools', () => { it('should return a successful result with tools', () => { const tools = getTools(); diff --git a/packages/agent/src/tools/getTools.ts b/packages/agent/src/tools/getTools.ts index 43d67cb..414599a 100644 --- a/packages/agent/src/tools/getTools.ts +++ b/packages/agent/src/tools/getTools.ts @@ -3,10 +3,12 @@ import { Tool } from '../core/types.js'; // Import tools import { browseMessageTool } from './browser/browseMessage.js'; import { browseStartTool } from './browser/browseStart.js'; -import { subAgentTool } from './interaction/subAgent.js'; +import { agentMessageTool } from './interaction/agentMessage.js'; +import { agentStartTool } from './interaction/agentStart.js'; import { userPromptTool } from './interaction/userPrompt.js'; import { fetchTool } from './io/fetch.js'; import { textEditorTool } from './io/textEditor.js'; +import { listBackgroundToolsTool } from './system/listBackgroundTools.js'; import { respawnTool } from './system/respawn.js'; import { sequenceCompleteTool } from './system/sequenceComplete.js'; import { shellMessageTool } from './system/shellMessage.js'; @@ -16,16 +18,17 @@ import { sleepTool } from './system/sleep.js'; // Import these separately to avoid circular dependencies interface GetToolsOptions { - enableUserPrompt?: boolean; + userPrompt?: boolean; } export function getTools(options?: GetToolsOptions): Tool[] { - const enableUserPrompt = options?.enableUserPrompt !== false; // Default to true if not specified + const userPrompt = options?.userPrompt !== false; // Default to true if not specified // Force cast to Tool type to avoid TypeScript issues const tools: Tool[] = [ textEditorTool as unknown as Tool, - subAgentTool as unknown as Tool, + agentStartTool as unknown as Tool, + agentMessageTool as unknown as Tool, sequenceCompleteTool as unknown as Tool, fetchTool as unknown as Tool, shellStartTool as unknown as Tool, @@ -34,10 +37,11 @@ export function getTools(options?: GetToolsOptions): Tool[] { browseMessageTool as unknown as Tool, respawnTool as unknown as Tool, sleepTool as unknown as Tool, + listBackgroundToolsTool as unknown as Tool, ]; // Only include userPrompt tool if enabled - if (enableUserPrompt) { + if (userPrompt) { tools.push(userPromptTool as unknown as Tool); } diff --git a/packages/agent/src/tools/interaction/agentMessage.ts b/packages/agent/src/tools/interaction/agentMessage.ts new file mode 100644 index 0000000..91e0afd --- /dev/null +++ b/packages/agent/src/tools/interaction/agentMessage.ts @@ -0,0 +1,159 @@ +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +import { + backgroundToolRegistry, + BackgroundToolStatus, +} from '../../core/backgroundTools.js'; +import { Tool } from '../../core/types.js'; + +import { agentStates } from './agentStart.js'; + +const parameterSchema = z.object({ + instanceId: z.string().describe('The ID returned by agentStart'), + guidance: z + .string() + .optional() + .describe('Optional guidance or instructions to send to the sub-agent'), + terminate: z + .boolean() + .optional() + .describe('Whether to terminate the sub-agent (default: false)'), + description: z + .string() + .describe('The reason for this agent interaction (max 80 chars)'), +}); + +const returnSchema = z.object({ + output: z.string().describe('The current output from the sub-agent'), + completed: z + .boolean() + .describe('Whether the sub-agent has completed its task'), + error: z + .string() + .optional() + .describe('Error message if the sub-agent encountered an error'), + terminated: z + .boolean() + .optional() + .describe('Whether the sub-agent was terminated by this message'), +}); + +type Parameters = z.infer; +type ReturnType = z.infer; + +export const agentMessageTool: Tool = { + name: 'agentMessage', + description: + 'Interacts with a running sub-agent, getting its current state and optionally providing guidance', + logPrefix: '🤖', + parameters: parameterSchema, + parametersJsonSchema: zodToJsonSchema(parameterSchema), + returns: returnSchema, + returnsJsonSchema: zodToJsonSchema(returnSchema), + + execute: async ( + { instanceId, guidance, terminate }, + { logger }, + ): Promise => { + logger.verbose( + `Interacting with sub-agent ${instanceId}${guidance ? ' with guidance' : ''}${terminate ? ' with termination request' : ''}`, + ); + + try { + const agentState = agentStates.get(instanceId); + if (!agentState) { + throw new Error(`No sub-agent found with ID ${instanceId}`); + } + + // Check if the agent was already terminated + if (agentState.aborted) { + return { + output: agentState.output || 'Sub-agent was previously terminated', + completed: true, + terminated: true, + }; + } + + // Terminate the agent if requested + if (terminate) { + agentState.aborted = true; + agentState.completed = true; + + // Update background tool registry with terminated status + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.TERMINATED, + { + terminatedByUser: true, + }, + ); + + return { + output: agentState.output || 'Sub-agent terminated before completion', + completed: true, + terminated: true, + }; + } + + // Add guidance to the agent state for future implementation + // In a more advanced implementation, this could inject the guidance + // into the agent's execution context + if (guidance) { + logger.info( + `Guidance provided to sub-agent ${instanceId}: ${guidance}`, + ); + // This is a placeholder for future implementation + // In a real implementation, we would need to interrupt the agent's + // execution and inject this guidance + } + + // Get the current output + const output = + agentState.result?.result || agentState.output || 'No output yet'; + + return { + output, + completed: agentState.completed, + ...(agentState.error && { error: agentState.error }), + }; + } catch (error) { + if (error instanceof Error) { + logger.verbose(`Sub-agent interaction failed: ${error.message}`); + + return { + output: '', + completed: false, + error: error.message, + }; + } + + const errorMessage = String(error); + logger.error( + `Unknown error during sub-agent interaction: ${errorMessage}`, + ); + return { + output: '', + completed: false, + error: `Unknown error occurred: ${errorMessage}`, + }; + } + }, + + logParameters: (input, { logger }) => { + logger.info( + `Interacting with sub-agent ${input.instanceId}, ${input.description}${input.terminate ? ' (terminating)' : ''}`, + ); + }, + logReturns: (output, { logger }) => { + if (output.error) { + logger.error(`Sub-agent interaction error: ${output.error}`); + } else if (output.terminated) { + logger.info('Sub-agent was terminated'); + } else if (output.completed) { + logger.info('Sub-agent has completed its task'); + } else { + logger.info('Sub-agent is still running'); + } + }, +}; diff --git a/packages/agent/src/tools/interaction/agentStart.ts b/packages/agent/src/tools/interaction/agentStart.ts new file mode 100644 index 0000000..ec106f3 --- /dev/null +++ b/packages/agent/src/tools/interaction/agentStart.ts @@ -0,0 +1,200 @@ +import { v4 as uuidv4 } from 'uuid'; +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +import { + backgroundToolRegistry, + BackgroundToolStatus, +} from '../../core/backgroundTools.js'; +import { + getDefaultSystemPrompt, + AgentConfig, +} from '../../core/toolAgent/config.js'; +import { toolAgent } from '../../core/toolAgent/toolAgentCore.js'; +import { ToolAgentResult } from '../../core/toolAgent/types.js'; +import { Tool, ToolContext } from '../../core/types.js'; +import { getTools } from '../getTools.js'; + +// Define AgentState type +type AgentState = { + goal: string; + prompt: string; + output: string; + completed: boolean; + error?: string; + result?: ToolAgentResult; + context: ToolContext; + workingDirectory: string; + tools: Tool[]; + aborted: boolean; +}; + +// Global map to store agent state +export const agentStates: Map = new Map(); + +const parameterSchema = z.object({ + description: z + .string() + .describe("A brief description of the sub-agent's purpose (max 80 chars)"), + goal: z + .string() + .describe('The main objective that the sub-agent needs to achieve'), + projectContext: z + .string() + .describe('Context about the problem or environment'), + workingDirectory: z + .string() + .optional() + .describe('The directory where the sub-agent should operate'), + relevantFilesDirectories: z + .string() + .optional() + .describe('A list of files, which may include ** or * wildcard characters'), + userPrompt: z + .boolean() + .optional() + .describe( + 'Whether to allow the sub-agent to use the userPrompt tool (default: false)', + ), +}); + +const returnSchema = z.object({ + instanceId: z.string().describe('The ID of the started agent process'), + status: z.string().describe('The initial status of the agent'), +}); + +type Parameters = z.infer; +type ReturnType = z.infer; + +// Sub-agent specific configuration +const subAgentConfig: AgentConfig = { + maxIterations: 200, + getSystemPrompt: (context: ToolContext) => { + return [ + getDefaultSystemPrompt(context), + 'You are a focused AI sub-agent handling a specific task.', + 'You have access to the same tools as the main agent but should focus only on your assigned task.', + 'When complete, call the sequenceComplete tool with your results.', + 'Follow any specific conventions or requirements provided in the task context.', + 'Ask the main agent for clarification if critical information is missing.', + ].join('\n'); + }, +}; + +export const agentStartTool: Tool = { + name: 'agentStart', + description: + 'Starts a sub-agent and returns an instance ID immediately for later interaction', + logPrefix: '🤖', + parameters: parameterSchema, + parametersJsonSchema: zodToJsonSchema(parameterSchema), + returns: returnSchema, + returnsJsonSchema: zodToJsonSchema(returnSchema), + execute: async (params, context) => { + const { logger, agentId } = context; + + // Validate parameters + const { + description, + goal, + projectContext, + workingDirectory, + relevantFilesDirectories, + userPrompt = false, + } = parameterSchema.parse(params); + + // Create an instance ID + const instanceId = uuidv4(); + + // Register this agent with the background tool registry + backgroundToolRegistry.registerAgent(agentId || 'unknown', goal); + logger.verbose(`Registered agent with ID: ${instanceId}`); + + // Construct a well-structured prompt + const prompt = [ + `Description: ${description}`, + `Goal: ${goal}`, + `Project Context: ${projectContext}`, + workingDirectory ? `Working Directory: ${workingDirectory}` : '', + relevantFilesDirectories + ? `Relevant Files:\n ${relevantFilesDirectories}` + : '', + ] + .filter(Boolean) + .join('\n'); + + const tools = getTools({ userPrompt }); + + // Store the agent state + const agentState: AgentState = { + goal, + prompt, + output: '', + completed: false, + context: { ...context }, + workingDirectory: workingDirectory ?? context.workingDirectory, + tools, + aborted: false, + }; + + agentStates.set(instanceId, agentState); + + // Start the agent in a separate promise that we don't await + // eslint-disable-next-line promise/catch-or-return + Promise.resolve().then(async () => { + try { + const result = await toolAgent(prompt, tools, subAgentConfig, { + ...context, + workingDirectory: workingDirectory ?? context.workingDirectory, + }); + + // Update agent state with the result + const state = agentStates.get(instanceId); + if (state && !state.aborted) { + state.completed = true; + state.result = result; + state.output = result.result; + + // Update background tool registry with completed status + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.COMPLETED, + { + result: + result.result.substring(0, 100) + + (result.result.length > 100 ? '...' : ''), + }, + ); + } + } catch (error) { + // Update agent state with the error + const state = agentStates.get(instanceId); + if (state && !state.aborted) { + state.completed = true; + state.error = error instanceof Error ? error.message : String(error); + + // Update background tool registry with error status + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.ERROR, + { + error: error instanceof Error ? error.message : String(error), + }, + ); + } + } + return true; + }); + + return { + instanceId, + status: 'Agent started successfully', + }; + }, + logParameters: (input, { logger }) => { + logger.info(`Starting sub-agent for task "${input.description}"`); + }, + logReturns: (output, { logger }) => { + logger.info(`Sub-agent started with instance ID: ${output.instanceId}`); + }, +}; diff --git a/packages/agent/src/tools/interaction/agentTools.test.ts b/packages/agent/src/tools/interaction/agentTools.test.ts new file mode 100644 index 0000000..9b0531e --- /dev/null +++ b/packages/agent/src/tools/interaction/agentTools.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { TokenTracker } from '../../core/tokens.js'; +import { ToolContext } from '../../core/types.js'; +import { MockLogger } from '../../utils/mockLogger.js'; + +import { agentMessageTool } from './agentMessage.js'; +import { agentStartTool, agentStates } from './agentStart.js'; + +// Mock the toolAgent function +vi.mock('../../core/toolAgent/toolAgentCore.js', () => ({ + toolAgent: vi.fn().mockResolvedValue({ + result: 'Mock agent result', + interactions: 1, + }), +})); + +// Mock context +const mockContext: ToolContext = { + logger: new MockLogger(), + tokenTracker: new TokenTracker(), + workingDirectory: '/test', + headless: true, + userSession: false, + pageFilter: 'none', + githubMode: true, + provider: 'anthropic', + model: 'claude-3-7-sonnet-20250219', + maxTokens: 4096, + temperature: 0.7, +}; + +describe('Agent Tools', () => { + describe('agentStartTool', () => { + it('should start an agent and return an instance ID', async () => { + const result = await agentStartTool.execute( + { + description: 'Test agent', + goal: 'Test the agent tools', + projectContext: 'Testing environment', + }, + mockContext, + ); + + expect(result).toHaveProperty('instanceId'); + expect(result).toHaveProperty('status'); + expect(result.status).toBe('Agent started successfully'); + + // Verify the agent state was created + expect(agentStates.has(result.instanceId)).toBe(true); + + const state = agentStates.get(result.instanceId); + expect(state).toHaveProperty('goal', 'Test the agent tools'); + expect(state).toHaveProperty('prompt'); + expect(state).toHaveProperty('completed', false); + expect(state).toHaveProperty('aborted', false); + }); + }); + + describe('agentMessageTool', () => { + it('should retrieve agent state', async () => { + // First start an agent + const startResult = await agentStartTool.execute( + { + description: 'Test agent for message', + goal: 'Test the agent message tool', + projectContext: 'Testing environment', + }, + mockContext, + ); + + // Then get its state + const messageResult = await agentMessageTool.execute( + { + instanceId: startResult.instanceId, + description: 'Checking agent status', + }, + mockContext, + ); + + expect(messageResult).toHaveProperty('output'); + expect(messageResult).toHaveProperty('completed', false); + }); + + it('should handle non-existent agent IDs', async () => { + const result = await agentMessageTool.execute( + { + instanceId: 'non-existent-id', + description: 'Checking non-existent agent', + }, + mockContext, + ); + + expect(result).toHaveProperty('error'); + expect(result.error).toContain('No sub-agent found with ID'); + }); + + it('should terminate an agent when requested', async () => { + // First start an agent + const startResult = await agentStartTool.execute( + { + description: 'Test agent for termination', + goal: 'Test agent termination', + projectContext: 'Testing environment', + }, + mockContext, + ); + + // Then terminate it + const messageResult = await agentMessageTool.execute( + { + instanceId: startResult.instanceId, + terminate: true, + description: 'Terminating agent', + }, + mockContext, + ); + + expect(messageResult).toHaveProperty('terminated', true); + expect(messageResult).toHaveProperty('completed', true); + + // Verify the agent state was updated + const state = agentStates.get(startResult.instanceId); + expect(state).toHaveProperty('aborted', true); + expect(state).toHaveProperty('completed', true); + }); + }); +}); diff --git a/packages/agent/src/tools/interaction/subAgent.test.ts b/packages/agent/src/tools/interaction/subAgent.test.ts new file mode 100644 index 0000000..4b4df8e --- /dev/null +++ b/packages/agent/src/tools/interaction/subAgent.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { TokenTracker } from '../../core/tokens.js'; +import { ToolContext } from '../../core/types.js'; +import { MockLogger } from '../../utils/mockLogger.js'; + +import { subAgentTool } from './subAgent.js'; + +// Mock the toolAgent function +vi.mock('../../core/toolAgent/toolAgentCore.js', () => ({ + toolAgent: vi.fn().mockResolvedValue({ + result: 'Mock sub-agent result', + interactions: 1, + }), +})); + +// Mock the getTools function +vi.mock('../getTools.js', () => ({ + getTools: vi.fn().mockReturnValue([{ name: 'mockTool' }]), +})); + +// Mock context +const mockContext: ToolContext = { + logger: new MockLogger(), + tokenTracker: new TokenTracker(), + workingDirectory: '/test', + headless: true, + userSession: false, + pageFilter: 'none', + githubMode: true, + provider: 'anthropic', + model: 'claude-3-7-sonnet-20250219', + maxTokens: 4096, + temperature: 0.7, +}; + +describe('subAgentTool', () => { + it('should create a sub-agent and return its response', async () => { + const result = await subAgentTool.execute( + { + description: 'Test sub-agent', + goal: 'Test the sub-agent tool', + projectContext: 'Testing environment', + }, + mockContext, + ); + + expect(result).toHaveProperty('response'); + expect(result.response).toBe('Mock sub-agent result'); + }); + + it('should use custom working directory when provided', async () => { + const { toolAgent } = await import('../../core/toolAgent/toolAgentCore.js'); + + await subAgentTool.execute( + { + description: 'Test sub-agent with custom directory', + goal: 'Test the sub-agent tool', + projectContext: 'Testing environment', + workingDirectory: '/custom/dir', + }, + mockContext, + ); + + // Verify toolAgent was called with the custom working directory + expect(toolAgent).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.any(Object), + expect.objectContaining({ + workingDirectory: '/custom/dir', + }), + ); + }); + + it('should include relevant files in the prompt when provided', async () => { + const { toolAgent } = await import('../../core/toolAgent/toolAgentCore.js'); + + await subAgentTool.execute( + { + description: 'Test sub-agent with relevant files', + goal: 'Test the sub-agent tool', + projectContext: 'Testing environment', + relevantFilesDirectories: 'src/**/*.ts', + }, + mockContext, + ); + + // Verify toolAgent was called with a prompt containing the relevant files + expect(toolAgent).toHaveBeenCalledWith( + expect.stringContaining('Relevant Files'), + expect.any(Array), + expect.any(Object), + expect.any(Object), + ); + }); +}); diff --git a/packages/agent/src/tools/interaction/subAgent.ts b/packages/agent/src/tools/interaction/subAgent.ts index de61d7d..3f66ae2 100644 --- a/packages/agent/src/tools/interaction/subAgent.ts +++ b/packages/agent/src/tools/interaction/subAgent.ts @@ -1,9 +1,13 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; +import { + backgroundToolRegistry, + BackgroundToolStatus, +} from '../../core/backgroundTools.js'; import { getDefaultSystemPrompt, - getModel, + AgentConfig, } from '../../core/toolAgent/config.js'; import { toolAgent } from '../../core/toolAgent/toolAgentCore.js'; import { Tool, ToolContext } from '../../core/types.js'; @@ -41,11 +45,8 @@ type Parameters = z.infer; type ReturnType = z.infer; // Sub-agent specific configuration -const subAgentConfig = { - maxIterations: 50, - model: getModel('anthropic', 'claude-3-7-sonnet-20250219'), - maxTokens: 4096, - temperature: 0.7, +const subAgentConfig: AgentConfig = { + maxIterations: 200, getSystemPrompt: (context: ToolContext) => { return [ getDefaultSystemPrompt(context), @@ -68,6 +69,8 @@ export const subAgentTool: Tool = { returns: returnSchema, returnsJsonSchema: zodToJsonSchema(returnSchema), execute: async (params, context) => { + const { logger, agentId } = context; + // Validate parameters const { description, @@ -77,6 +80,13 @@ export const subAgentTool: Tool = { relevantFilesDirectories, } = parameterSchema.parse(params); + // Register this sub-agent with the background tool registry + const subAgentId = backgroundToolRegistry.registerAgent( + agentId || 'unknown', + goal, + ); + logger.verbose(`Registered sub-agent with ID: ${subAgentId}`); + // Construct a well-structured prompt const prompt = [ `Description: ${description}`, @@ -90,18 +100,43 @@ export const subAgentTool: Tool = { .filter(Boolean) .join('\n'); - const tools = getTools({ enableUserPrompt: false }); + const tools = getTools({ userPrompt: false }); - // Update config if timeout is specified - const config = { + // Use the subAgentConfig + const config: AgentConfig = { ...subAgentConfig, }; - const result = await toolAgent(prompt, tools, config, { - ...context, - workingDirectory: workingDirectory ?? context.workingDirectory, - }); - return { response: result.result }; + try { + const result = await toolAgent(prompt, tools, config, { + ...context, + workingDirectory: workingDirectory ?? context.workingDirectory, + }); + + // Update background tool registry with completed status + backgroundToolRegistry.updateToolStatus( + subAgentId, + BackgroundToolStatus.COMPLETED, + { + result: + result.result.substring(0, 100) + + (result.result.length > 100 ? '...' : ''), + }, + ); + + return { response: result.result }; + } catch (error) { + // Update background tool registry with error status + backgroundToolRegistry.updateToolStatus( + subAgentId, + BackgroundToolStatus.ERROR, + { + error: error instanceof Error ? error.message : String(error), + }, + ); + + throw error; + } }, logParameters: (input, { logger }) => { logger.info(`Delegating task "${input.description}"`); diff --git a/packages/agent/src/tools/interaction/userPrompt.test.ts b/packages/agent/src/tools/interaction/userPrompt.test.ts new file mode 100644 index 0000000..756acbb --- /dev/null +++ b/packages/agent/src/tools/interaction/userPrompt.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { ToolContext } from '../../core/types.js'; +import { getMockToolContext } from '../getTools.test.js'; + +import { userPromptTool } from './userPrompt.js'; + +// Mock the userPrompt function +vi.mock('../../utils/userPrompt.js', () => ({ + userPrompt: vi.fn().mockResolvedValue('Mock user response'), +})); + +// Mock context +const toolContext: ToolContext = getMockToolContext(); +describe('userPromptTool', () => { + it('should prompt the user and return their response', async () => { + const result = await userPromptTool.execute( + { + prompt: 'Test prompt', + }, + toolContext, + ); + + expect(result).toHaveProperty('userText'); + expect(result.userText).toBe('Mock user response'); + + // Since we're using MockLogger which doesn't track calls, + // we can't verify the exact logger calls, but the test is still valid + }); + + it('should log the user response', async () => { + const { userPrompt } = await import('../../utils/userPrompt.js'); + (userPrompt as any).mockResolvedValueOnce('Custom response'); + + const result = await userPromptTool.execute( + { + prompt: 'Another test prompt', + }, + toolContext, + ); + + expect(result.userText).toBe('Custom response'); + + // Since we're using MockLogger which doesn't track calls, + // we can't verify the exact logger calls, but the test is still valid + }); +}); diff --git a/packages/agent/src/tools/io/textEditor.test.ts b/packages/agent/src/tools/io/textEditor.test.ts index 4b7f974..0bae64d 100644 --- a/packages/agent/src/tools/io/textEditor.test.ts +++ b/packages/agent/src/tools/io/textEditor.test.ts @@ -3,24 +3,16 @@ import { mkdtemp, readFile } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { TokenTracker } from '../../core/tokens.js'; import { ToolContext } from '../../core/types.js'; import { MockLogger } from '../../utils/mockLogger.js'; +import { getMockToolContext } from '../getTools.test.js'; import { shellExecuteTool } from '../system/shellExecute.js'; import { textEditorTool } from './textEditor.js'; -const toolContext: ToolContext = { - logger: new MockLogger(), - headless: true, - workingDirectory: '.', - userSession: false, - pageFilter: 'simple', - tokenTracker: new TokenTracker(), - githubMode: false, -}; +const toolContext: ToolContext = getMockToolContext(); describe('textEditor', () => { let testDir: string; @@ -303,4 +295,153 @@ describe('textEditor', () => { ); }).rejects.toThrow(/Found 2 occurrences/); }); + + it('should overwrite an existing file with create command', async () => { + const initialContent = 'Initial content'; + const newContent = 'New content that overwrites the file'; + const testPath = join(testDir, `${randomUUID()}.txt`); + + // Create initial file + await textEditorTool.execute( + { + command: 'create', + path: testPath, + file_text: initialContent, + description: 'test', + }, + toolContext, + ); + + // Verify initial content + let content = await readFile(testPath, 'utf8'); + expect(content).toBe(initialContent); + + // Overwrite the file using create command + const result = await textEditorTool.execute( + { + command: 'create', + path: testPath, + file_text: newContent, + description: 'test', + }, + toolContext, + ); + + // Verify return value + expect(result.success).toBe(true); + expect(result.message).toContain('File overwritten'); + + // Verify content has been updated + content = await readFile(testPath, 'utf8'); + expect(content).toBe(newContent); + }); + + it('should be able to undo file overwrite', async () => { + const initialContent = 'Initial content that will be restored'; + const overwrittenContent = 'This content will be undone'; + const testPath = join(testDir, `${randomUUID()}.txt`); + + // Create initial file + await textEditorTool.execute( + { + command: 'create', + path: testPath, + file_text: initialContent, + description: 'test', + }, + toolContext, + ); + + // Overwrite the file + await textEditorTool.execute( + { + command: 'create', + path: testPath, + file_text: overwrittenContent, + description: 'test', + }, + toolContext, + ); + + // Verify overwritten content + let content = await readFile(testPath, 'utf8'); + expect(content).toBe(overwrittenContent); + + // Undo the overwrite + const result = await textEditorTool.execute( + { + command: 'undo_edit', + path: testPath, + description: 'test', + }, + toolContext, + ); + + // Verify return value + expect(result.success).toBe(true); + expect(result.message).toContain('Successfully reverted'); + + // Verify content is back to initial + content = await readFile(testPath, 'utf8'); + expect(content).toBe(initialContent); + }); + + it('should convert absolute paths to relative paths in log messages', () => { + // Create a mock logger with a spy on the info method + const mockLogger = new MockLogger(); + const infoSpy = vi.spyOn(mockLogger, 'info'); + + // Create a context with a specific working directory + const contextWithWorkingDir: ToolContext = { + ...toolContext, + logger: mockLogger, + workingDirectory: '/home/user/project', + }; + + // Test with an absolute path within the working directory + const absolutePath = '/home/user/project/packages/agent/src/file.ts'; + textEditorTool.logParameters?.( + { + command: 'view', + path: absolutePath, + description: 'test path conversion', + }, + contextWithWorkingDir, + ); + + // Verify the log message contains the relative path + expect(infoSpy).toHaveBeenCalledWith( + expect.stringContaining('./packages/agent/src/file.ts'), + ); + + // Test with an absolute path outside the working directory + infoSpy.mockClear(); + const externalPath = '/etc/config.json'; + textEditorTool.logParameters?.( + { + command: 'view', + path: externalPath, + description: 'test external path', + }, + contextWithWorkingDir, + ); + + // Verify the log message keeps the absolute path + expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining(externalPath)); + + // Test with a relative path + infoSpy.mockClear(); + const relativePath = 'src/file.ts'; + textEditorTool.logParameters?.( + { + command: 'view', + path: relativePath, + description: 'test relative path', + }, + contextWithWorkingDir, + ); + + // Verify the log message keeps the relative path as is + expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining(relativePath)); + }); }); diff --git a/packages/agent/src/tools/io/textEditor.ts b/packages/agent/src/tools/io/textEditor.ts index ce31909..f881ed9 100644 --- a/packages/agent/src/tools/io/textEditor.ts +++ b/packages/agent/src/tools/io/textEditor.ts @@ -160,13 +160,6 @@ export const textEditorTool: Tool = { } case 'create': { - // Check if file already exists - if (fsSync.existsSync(absolutePath)) { - throw new Error( - `File already exists: ${filePath}. Use str_replace to modify it.`, - ); - } - if (!file_text) { throw new Error('file_text parameter is required for create command'); } @@ -174,15 +167,29 @@ export const textEditorTool: Tool = { // Create parent directories if they don't exist await fs.mkdir(path.dirname(absolutePath), { recursive: true }); - // Create the file - await fs.writeFile(absolutePath, file_text, 'utf8'); + // Check if file already exists + const fileExists = fsSync.existsSync(absolutePath); + + if (fileExists) { + // Save current state for undo if file exists + const currentContent = await fs.readFile(absolutePath, 'utf8'); + if (!fileStateHistory[absolutePath]) { + fileStateHistory[absolutePath] = []; + } + fileStateHistory[absolutePath].push(currentContent); + } else { + // Initialize history for new files + fileStateHistory[absolutePath] = []; + } - // Store initial state for undo - fileStateHistory[absolutePath] = [file_text]; + // Create or overwrite the file + await fs.writeFile(absolutePath, file_text, 'utf8'); return { success: true, - message: `File created: ${filePath}`, + message: fileExists + ? `File overwritten: ${filePath}` + : `File created: ${filePath}`, }; } @@ -295,9 +302,19 @@ export const textEditorTool: Tool = { throw new Error(`Unknown command: ${command}`); } }, - logParameters: (input, { logger }) => { + logParameters: (input, { logger, workingDirectory }) => { + // Convert absolute path to relative path if possible + let displayPath = input.path; + if (workingDirectory && path.isAbsolute(input.path)) { + // Check if the path is within the working directory + if (input.path.startsWith(workingDirectory)) { + // Convert to relative path with ./ prefix + displayPath = './' + path.relative(workingDirectory, input.path); + } + } + logger.info( - `${input.command} operation on "${input.path}", ${input.description}`, + `${input.command} operation on "${displayPath}", ${input.description}`, ); }, logReturns: (result, { logger }) => { diff --git a/packages/agent/src/tools/system/listBackgroundTools.test.ts b/packages/agent/src/tools/system/listBackgroundTools.test.ts new file mode 100644 index 0000000..5f9fddf --- /dev/null +++ b/packages/agent/src/tools/system/listBackgroundTools.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { listBackgroundToolsTool } from './listBackgroundTools.js'; + +// Mock the entire background tools module +vi.mock('../../core/backgroundTools.js', () => { + return { + backgroundToolRegistry: { + getToolsByAgent: vi.fn().mockReturnValue([ + { + id: 'shell-1', + type: 'shell', + status: 'running', + startTime: new Date(Date.now() - 10000), + agentId: 'agent-1', + metadata: { command: 'ls -la' }, + }, + ]), + }, + BackgroundToolStatus: { + RUNNING: 'running', + COMPLETED: 'completed', + ERROR: 'error', + TERMINATED: 'terminated', + }, + BackgroundToolType: { + SHELL: 'shell', + BROWSER: 'browser', + AGENT: 'agent', + }, + }; +}); + +describe('listBackgroundTools tool', () => { + const mockLogger = { + debug: vi.fn(), + verbose: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; + + it('should list background tools', async () => { + const result = await listBackgroundToolsTool.execute({}, { + logger: mockLogger as any, + agentId: 'agent-1', + } as any); + + expect(result.count).toEqual(1); + expect(result.tools).toHaveLength(1); + }); +}); diff --git a/packages/agent/src/tools/system/listBackgroundTools.ts b/packages/agent/src/tools/system/listBackgroundTools.ts new file mode 100644 index 0000000..83eff8f --- /dev/null +++ b/packages/agent/src/tools/system/listBackgroundTools.ts @@ -0,0 +1,118 @@ +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; + +import { + backgroundToolRegistry, + BackgroundToolStatus, +} from '../../core/backgroundTools.js'; +import { Tool } from '../../core/types.js'; + +const parameterSchema = z.object({ + status: z + .enum(['all', 'running', 'completed', 'error', 'terminated']) + .optional() + .describe('Filter tools by status (default: "all")'), + type: z + .enum(['all', 'shell', 'browser', 'agent']) + .optional() + .describe('Filter tools by type (default: "all")'), + verbose: z + .boolean() + .optional() + .describe('Include detailed metadata about each tool (default: false)'), +}); + +const returnSchema = z.object({ + tools: z.array( + z.object({ + id: z.string(), + type: z.string(), + status: z.string(), + startTime: z.string(), + endTime: z.string().optional(), + runtime: z.number().describe('Runtime in seconds'), + metadata: z.record(z.any()).optional(), + }), + ), + count: z.number(), +}); + +type Parameters = z.infer; +type ReturnType = z.infer; + +export const listBackgroundToolsTool: Tool = { + name: 'listBackgroundTools', + description: + 'Lists all background tools (shells, browsers, agents) and their status', + logPrefix: '🔍', + parameters: parameterSchema, + returns: returnSchema, + parametersJsonSchema: zodToJsonSchema(parameterSchema), + returnsJsonSchema: zodToJsonSchema(returnSchema), + + execute: async ( + { status = 'all', type = 'all', verbose = false }, + { logger, agentId }, + ): Promise => { + logger.verbose( + `Listing background tools with status: ${status}, type: ${type}, verbose: ${verbose}`, + ); + + // Get all tools for this agent + const tools = backgroundToolRegistry.getToolsByAgent(agentId || 'unknown'); + + // Filter by status if specified + const filteredByStatus = + status === 'all' + ? tools + : tools.filter((tool) => { + const statusEnum = + status.toUpperCase() as keyof typeof BackgroundToolStatus; + return tool.status === BackgroundToolStatus[statusEnum]; + }); + + // Filter by type if specified + const filteredTools = + type === 'all' + ? filteredByStatus + : filteredByStatus.filter( + (tool) => tool.type.toLowerCase() === type.toLowerCase(), + ); + + // Format the response + const formattedTools = filteredTools.map((tool) => { + const now = new Date(); + const startTime = tool.startTime; + const endTime = tool.endTime || now; + const runtime = (endTime.getTime() - startTime.getTime()) / 1000; // in seconds + + return { + id: tool.id, + type: tool.type, + status: tool.status, + startTime: startTime.toISOString(), + ...(tool.endTime && { endTime: tool.endTime.toISOString() }), + runtime: parseFloat(runtime.toFixed(2)), + ...(verbose && { metadata: tool.metadata }), + }; + }); + + return { + tools: formattedTools, + count: formattedTools.length, + }; + }, + + logParameters: ( + { status = 'all', type = 'all', verbose = false }, + { logger }, + ) => { + logger.info( + `Listing ${type} background tools with status: ${status}, verbose: ${verbose}`, + ); + }, + + logReturns: (output, { logger }) => { + logger.info(`Found ${output.count} background tools`); + }, +}; diff --git a/packages/agent/src/tools/system/respawn.test.ts b/packages/agent/src/tools/system/respawn.test.ts index ed968b4..b70ea64 100644 --- a/packages/agent/src/tools/system/respawn.test.ts +++ b/packages/agent/src/tools/system/respawn.test.ts @@ -1,20 +1,12 @@ import { describe, it, expect } from 'vitest'; -import { TokenTracker } from '../../core/tokens'; import { ToolContext } from '../../core/types'; -import { MockLogger } from '../../utils/mockLogger'; +import { getMockToolContext } from '../getTools.test'; import { respawnTool } from './respawn'; -const toolContext: ToolContext = { - logger: new MockLogger(), - headless: true, - workingDirectory: '.', - userSession: false, - pageFilter: 'simple', - tokenTracker: new TokenTracker(), - githubMode: false, -}; +const toolContext: ToolContext = getMockToolContext(); + describe('respawnTool', () => { it('should have correct name and description', () => { expect(respawnTool.name).toBe('respawn'); diff --git a/packages/agent/src/tools/system/shellExecute.test.ts b/packages/agent/src/tools/system/shellExecute.test.ts index 16715a7..50fe322 100644 --- a/packages/agent/src/tools/system/shellExecute.test.ts +++ b/packages/agent/src/tools/system/shellExecute.test.ts @@ -1,20 +1,11 @@ import { describe, it, expect } from 'vitest'; -import { TokenTracker } from '../../core/tokens.js'; import { ToolContext } from '../../core/types.js'; -import { MockLogger } from '../../utils/mockLogger.js'; +import { getMockToolContext } from '../getTools.test.js'; import { shellExecuteTool } from './shellExecute.js'; -const toolContext: ToolContext = { - logger: new MockLogger(), - headless: true, - workingDirectory: '.', - userSession: false, - pageFilter: 'simple', - tokenTracker: new TokenTracker(), - githubMode: false, -}; +const toolContext: ToolContext = getMockToolContext(); describe('shellExecute', () => { it('should execute shell commands', async () => { diff --git a/packages/agent/src/tools/system/shellMessage.test.ts b/packages/agent/src/tools/system/shellMessage.test.ts index 24b1061..b78da0e 100644 --- a/packages/agent/src/tools/system/shellMessage.test.ts +++ b/packages/agent/src/tools/system/shellMessage.test.ts @@ -1,22 +1,13 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { TokenTracker } from '../../core/tokens.js'; import { ToolContext } from '../../core/types.js'; -import { MockLogger } from '../../utils/mockLogger.js'; import { sleep } from '../../utils/sleep.js'; +import { getMockToolContext } from '../getTools.test.js'; import { shellMessageTool, NodeSignals } from './shellMessage.js'; import { processStates, shellStartTool } from './shellStart.js'; -const toolContext: ToolContext = { - logger: new MockLogger(), - headless: true, - workingDirectory: '.', - userSession: false, - pageFilter: 'simple', - tokenTracker: new TokenTracker(), - githubMode: false, -}; +const toolContext: ToolContext = getMockToolContext(); // Helper function to get instanceId from shellStart result const getInstanceId = ( @@ -65,9 +56,13 @@ describe('shellMessageTool', () => { toolContext, ); + // With 'cat', the input should be echoed back exactly expect(result.stdout).toBe('hello world'); expect(result.stderr).toBe(''); expect(result.completed).toBe(false); + + // Verify the instance ID is valid + expect(processStates.has(testInstanceId)).toBe(true); }); it('should handle nonexistent process', async () => { diff --git a/packages/agent/src/tools/system/shellMessage.ts b/packages/agent/src/tools/system/shellMessage.ts index 49f3f7c..fc892ac 100644 --- a/packages/agent/src/tools/system/shellMessage.ts +++ b/packages/agent/src/tools/system/shellMessage.ts @@ -1,6 +1,10 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; +import { + backgroundToolRegistry, + BackgroundToolStatus, +} from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; import { sleep } from '../../utils/sleep.js'; @@ -109,17 +113,53 @@ export const shellMessageTool: Tool = { // Send signal if provided if (signal) { - const wasKilled = processState.process.kill(signal); - if (!wasKilled) { - return { - stdout: '', - stderr: '', - completed: processState.state.completed, - signaled: false, - error: `Failed to send signal ${signal} to process (process may have already terminated)`, - }; + try { + processState.process.kill(signal); + // Mark as signaled regardless of process status + processState.state.signaled = true; + } catch (error) { + // If the process is already terminated, we'll just mark it as signaled anyway + processState.state.signaled = true; + + // Update background tool registry if signal failed + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.ERROR, + { + error: `Failed to send signal ${signal}: ${String(error)}`, + signalAttempted: signal, + }, + ); + + logger.verbose( + `Failed to send signal ${signal}: ${String(error)}, but marking as signaled anyway`, + ); + } + + // Update background tool registry with signal information + if ( + signal === 'SIGTERM' || + signal === 'SIGKILL' || + signal === 'SIGINT' + ) { + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.TERMINATED, + { + signal, + terminatedByUser: true, + }, + ); + } else { + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.RUNNING, + { + signal, + signaled: true, + }, + ); } - processState.state.signaled = true; } // Send input if provided @@ -135,7 +175,13 @@ export const shellMessageTool: Tool = { logger.info(`[${instanceId}] stdin: ${stdin}`); } + // No special handling for 'cat' command - let the actual process handle the echo + processState.process.stdin.write(`${stdin}\n`); + + // For interactive processes like 'cat', we need to give them time to process + // and echo back the input before clearing the buffer + await sleep(300); } // Wait a brief moment for output to be processed diff --git a/packages/agent/src/tools/system/shellStart.test.ts b/packages/agent/src/tools/system/shellStart.test.ts index 1abad12..223560c 100644 --- a/packages/agent/src/tools/system/shellStart.test.ts +++ b/packages/agent/src/tools/system/shellStart.test.ts @@ -1,21 +1,13 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { TokenTracker } from '../../core/tokens.js'; import { ToolContext } from '../../core/types.js'; -import { MockLogger } from '../../utils/mockLogger.js'; import { sleep } from '../../utils/sleep.js'; +import { getMockToolContext } from '../getTools.test.js'; import { processStates, shellStartTool } from './shellStart.js'; -const toolContext: ToolContext = { - logger: new MockLogger(), - headless: true, - workingDirectory: '.', - userSession: false, - pageFilter: 'simple', - tokenTracker: new TokenTracker(), - githubMode: false, -}; +const toolContext: ToolContext = getMockToolContext(); + describe('shellStartTool', () => { beforeEach(() => { processStates.clear(); diff --git a/packages/agent/src/tools/system/shellStart.ts b/packages/agent/src/tools/system/shellStart.ts index 231c50b..fa8e36d 100644 --- a/packages/agent/src/tools/system/shellStart.ts +++ b/packages/agent/src/tools/system/shellStart.ts @@ -4,6 +4,10 @@ import { v4 as uuidv4 } from 'uuid'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; +import { + backgroundToolRegistry, + BackgroundToolStatus, +} from '../../core/backgroundTools.js'; import { Tool } from '../../core/types.js'; import { errorToString } from '../../utils/errorToString.js'; @@ -25,6 +29,7 @@ type ProcessState = { }; // Global map to store process state +// This is exported so it can be accessed for cleanup export const processStates: Map = new Map(); const parameterSchema = z.object({ @@ -97,7 +102,7 @@ export const shellStartTool: Tool = { showStdIn = false, showStdout = false, }, - { logger, workingDirectory }, + { logger, workingDirectory, agentId }, ): Promise => { if (showStdIn) { logger.info(`Command input: ${command}`); @@ -106,11 +111,17 @@ export const shellStartTool: Tool = { return new Promise((resolve) => { try { + // Generate a unique ID for this process const instanceId = uuidv4(); + + // Register this shell process with the background tool registry + backgroundToolRegistry.registerShell(agentId || 'unknown', command); + let hasResolved = false; // Split command into command and args // Use command directly with shell: true + // Use shell option instead of explicit shell path to avoid platform-specific issues const process = spawn(command, [], { shell: true, cwd: workingDirectory, @@ -134,25 +145,33 @@ export const shellStartTool: Tool = { process.stdout.on('data', (data) => { const output = data.toString(); processState.stdout.push(output); - logger.verbose(`[${instanceId}] stdout: ${output.trim()}`); - if (processState.showStdout) { - logger.info(`[${instanceId}] stdout: ${output.trim()}`); - } + logger[processState.showStdout ? 'info' : 'verbose']( + `[${instanceId}] stdout: ${output.trim()}`, + ); }); if (process.stderr) process.stderr.on('data', (data) => { const output = data.toString(); processState.stderr.push(output); - logger.verbose(`[${instanceId}] stderr: ${output.trim()}`); - if (processState.showStdout) { - logger.info(`[${instanceId}] stderr: ${output.trim()}`); - } + logger[processState.showStdout ? 'info' : 'verbose']( + `[${instanceId}] stderr: ${output.trim()}`, + ); }); process.on('error', (error) => { logger.error(`[${instanceId}] Process error: ${error.message}`); processState.state.completed = true; + + // Update background tool registry with error status + backgroundToolRegistry.updateToolStatus( + instanceId, + BackgroundToolStatus.ERROR, + { + error: error.message, + }, + ); + if (!hasResolved) { hasResolved = true; resolve({ @@ -174,6 +193,18 @@ export const shellStartTool: Tool = { processState.state.signaled = signal !== null; processState.state.exitCode = code; + // Update background tool registry with completed status + const status = + code === 0 + ? BackgroundToolStatus.COMPLETED + : BackgroundToolStatus.ERROR; + backgroundToolRegistry.updateToolStatus(instanceId, status, { + exitCode: code, + signaled: signal !== null, + }); + + // For test environment with timeout=0, we should still return sync results + // when the process completes quickly if (!hasResolved) { hasResolved = true; // If we haven't resolved yet, this happened within the timeout @@ -190,18 +221,30 @@ export const shellStartTool: Tool = { } }); - // Set timeout to switch to async mode - setTimeout(() => { - if (!hasResolved) { - hasResolved = true; - resolve({ - mode: 'async', - instanceId, - stdout: processState.stdout.join('').trim(), - stderr: processState.stderr.join('').trim(), - }); - } - }, timeout); + // For test environment, when timeout is explicitly set to 0, we want to force async mode + if (timeout === 0) { + // Force async mode immediately + hasResolved = true; + resolve({ + mode: 'async', + instanceId, + stdout: processState.stdout.join('').trim(), + stderr: processState.stderr.join('').trim(), + }); + } else { + // Set timeout to switch to async mode after the specified timeout + setTimeout(() => { + if (!hasResolved) { + hasResolved = true; + resolve({ + mode: 'async', + instanceId, + stdout: processState.stdout.join('').trim(), + stderr: processState.stderr.join('').trim(), + }); + } + }, timeout); + } } catch (error) { logger.error(`Failed to start process: ${errorToString(error)}`); resolve({ diff --git a/packages/agent/src/tools/system/sleep.test.ts b/packages/agent/src/tools/system/sleep.test.ts index 8243769..17248a1 100644 --- a/packages/agent/src/tools/system/sleep.test.ts +++ b/packages/agent/src/tools/system/sleep.test.ts @@ -1,20 +1,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { TokenTracker } from '../../core/tokens'; import { ToolContext } from '../../core/types'; -import { MockLogger } from '../../utils/mockLogger'; +import { getMockToolContext } from '../getTools.test'; import { sleepTool } from './sleep'; -const toolContext: ToolContext = { - logger: new MockLogger(), - headless: true, - workingDirectory: '.', - userSession: false, - pageFilter: 'simple', - tokenTracker: new TokenTracker(), - githubMode: false, -}; +const toolContext: ToolContext = getMockToolContext(); describe('sleep tool', () => { beforeEach(() => { diff --git a/packages/agent/src/utils/errors.ts b/packages/agent/src/utils/errors.ts index 5276381..b41d63f 100644 --- a/packages/agent/src/utils/errors.ts +++ b/packages/agent/src/utils/errors.ts @@ -7,12 +7,11 @@ export const providerConfig: Record< keyName: 'ANTHROPIC_API_KEY', docsUrl: 'https://mycoder.ai/docs/getting-started/anthropic', }, - /* openai: { keyName: 'OPENAI_API_KEY', docsUrl: 'https://mycoder.ai/docs/getting-started/openai', }, - xai: { + /*xai: { keyName: 'XAI_API_KEY', docsUrl: 'https://mycoder.ai/docs/getting-started/xai', }, @@ -21,7 +20,7 @@ export const providerConfig: Record< docsUrl: 'https://mycoder.ai/docs/getting-started/mistral', },*/ // No API key needed for ollama as it uses a local server - //ollama: undefined, + ollama: undefined, }; /** diff --git a/packages/cli/.releaserc.json b/packages/cli/.releaserc.json index 9c41120..5c32972 100644 --- a/packages/cli/.releaserc.json +++ b/packages/cli/.releaserc.json @@ -5,7 +5,7 @@ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", - "@semantic-release/npm", + "@anolilab/semantic-release-pnpm", [ "@semantic-release/git", { diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 71a0c34..718d89d 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,3 +1,20 @@ +# mycoder-v1.0.0 (2025-03-11) + +### Bug Fixes + +- add deepmerge to cli package.json ([ab66377](https://github.com/drivecore/mycoder/commit/ab66377342c9f23fa874d2776e73d365141e8801)) +- don't save consent when using --userWarning=false ([41cf69d](https://github.com/drivecore/mycoder/commit/41cf69dee22acc31cd0f2aa9f80e36cd867fb20b)) +- **monorepo:** implement semantic-release-monorepo for proper versioning of sub-packages ([96c6284](https://github.com/drivecore/mycoder/commit/96c62848fbc3a4c1c591f3fd6202486e6461c4f2)) +- update hierarchical configuration system to fix failing tests ([93d949c](https://github.com/drivecore/mycoder/commit/93d949c03b7ebe96bad36713f6476c38d2a35224)) + +### Features + +- add --githubMode and --userPrompt as boolean CLI options that override config settings ([0390f94](https://github.com/drivecore/mycoder/commit/0390f94651e40de93a8cb9486a056a0b9cb2e165)) +- add CLI options for automated usage scenarios ([00419bc](https://github.com/drivecore/mycoder/commit/00419bc3e060db6d0c18fc72e2d7b6957791c875)) +- add maxTokens and temperature config options to CLI ([b461d3b](https://github.com/drivecore/mycoder/commit/b461d3b71b686d7679ecac62c0c66cc5a1df8fec)), closes [#118](https://github.com/drivecore/mycoder/issues/118) +- implement hierarchical configuration system ([84d73d1](https://github.com/drivecore/mycoder/commit/84d73d1e6324670890a203f455fe257aeb6ed07a)), closes [#153](https://github.com/drivecore/mycoder/issues/153) +- remove modelProvider and modelName - instant decrepation ([59834dc](https://github.com/drivecore/mycoder/commit/59834dcf932051a5c75624bd6f6ab12254f43769)) + # mycoder ## [0.7.0](https://github.com/drivecore/mycoder/compare/v0.6.1...v0.7.0) (2025-03-10) diff --git a/packages/cli/COMMIT_CONVENTION.md b/packages/cli/COMMIT_CONVENTION.md index 441891b..b66d38d 100644 --- a/packages/cli/COMMIT_CONVENTION.md +++ b/packages/cli/COMMIT_CONVENTION.md @@ -51,5 +51,3 @@ Commit messages are used to: 1. Automatically determine the next version number 2. Generate changelog entries 3. Create GitHub releases - -The process is automated through GitHub Actions and uses changesets for release management. diff --git a/packages/cli/README.md b/packages/cli/README.md index d07369c..6a48d73 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -38,8 +38,8 @@ mycoder --userPrompt false "Generate a basic Express.js server" # Disable user consent warning and version upgrade check for automated environments mycoder --userWarning false --upgradeCheck false "Generate a basic Express.js server" -# Enable GitHub mode -mycoder config set githubMode true +# Enable GitHub mode via CLI option (overrides config file) +mycoder --githubMode true ``` ## GitHub Mode @@ -52,53 +52,104 @@ MyCoder includes a GitHub mode that enables the agent to work with GitHub issues - Create PRs when work is complete - Create additional GitHub issues for follow-up tasks or ideas -To enable GitHub mode: +GitHub mode is **enabled by default** but requires the Git and GitHub CLI tools to be installed and configured: + +- Git CLI (`git`) must be installed +- GitHub CLI (`gh`) must be installed and authenticated + +MyCoder will automatically check for these requirements when GitHub mode is enabled and will: +- Warn you if any requirements are missing +- Automatically disable GitHub mode if the required tools are not available or not authenticated + +To manually enable/disable GitHub mode: + +1. Via CLI option (overrides config file): ```bash -mycoder config set githubMode true +mycoder --githubMode true # Enable GitHub mode +mycoder --githubMode false # Disable GitHub mode ``` -To disable GitHub mode: +2. Via configuration file: -```bash -mycoder config set githubMode false +```js +// mycoder.config.js +export default { + githubMode: true, // Enable GitHub mode (default) + // other configuration options... +}; ``` Requirements for GitHub mode: +- Git CLI (`git`) needs to be installed - GitHub CLI (`gh`) needs to be installed and authenticated - User needs to have appropriate GitHub permissions for the target repository +If GitHub mode is enabled but the requirements are not met, MyCoder will provide instructions on how to install and configure the missing tools. + ## Configuration -MyCoder stores configuration in `~/.mycoder/config.json`. You can manage configuration using the `config` command: +MyCoder is configured using a `mycoder.config.js` file in your project root, similar to ESLint and other modern JavaScript tools. This file exports a configuration object with your preferred settings. -```bash -# List all configuration -mycoder config list +You can create a `mycoder.config.js` file in your project root with your preferred settings. + +Example configuration file: + +```javascript +// mycoder.config.js +export default { + // GitHub integration + githubMode: true, -# Get a specific configuration value -mycoder config get githubMode + // Browser settings + headless: true, + userSession: false, + pageFilter: 'none', // 'simple', 'none', or 'readability' -# Set a configuration value -mycoder config set githubMode true + // Model settings + provider: 'anthropic', + model: 'claude-3-7-sonnet-20250219', + maxTokens: 4096, + temperature: 0.7, -# Reset a configuration value to its default -mycoder config clear customPrompt + // Custom settings + customPrompt: '', + profile: false, + tokenCache: true, + // API keys (better to use environment variables for these) + // ANTHROPIC_API_KEY: 'your-api-key', +}; ``` +MyCoder will search for configuration in the following places (in order of precedence): + +1. CLI options (e.g., `--githubMode true`) +2. Configuration file (`mycoder.config.js`, `.mycoderrc`, etc.) +3. Default values + ### Model Selection NOTE: Anthropic Claude 3.7 works the best by far in our testing. -MyCoder supports Anthropic, OpenAI, xAI/Grok, Mistral AI, and Ollama models. You can configure which model provider and model name to use with the following commands: +MyCoder supports Anthropic, OpenAI, xAI/Grok, Mistral AI, and Ollama models. You can configure which model provider and model name to use either via CLI options or in your configuration file: ```bash -# Use Anthropic models [These work the best at this time] -mycoder config set provider anthropic -mycoder config set model claude-3-7-sonnet-20250219 # or any other Anthropic model +# Via CLI options (overrides config file) +mycoder --provider anthropic --model claude-3-7-sonnet-20250219 "Your prompt here" +``` + +Or in your configuration file: +```js +// mycoder.config.js +export default { + // Model settings + provider: 'anthropic', + model: 'claude-3-7-sonnet-20250219', // or any other Anthropic model + // other configuration options... +}; ``` ### Available Configuration Options @@ -116,25 +167,35 @@ These options are available only as command-line parameters and are not stored i - `userWarning`: Skip user consent check for current session without saving consent (default: `true`) - `upgradeCheck`: Disable version upgrade check for automated/remote usage (default: `true`) -- `userPrompt`/`enableUserPrompt`: Enable or disable the userPrompt tool (default: `true`) +- `userPrompt`: Enable or disable the userPrompt tool (default: `true`) -Example: +Example configuration in `mycoder.config.js`: -```bash -# Set browser to show UI -mycoder config set headless false +```js +// mycoder.config.js +export default { + // Browser settings + headless: false, // Show browser UI + userSession: true, // Use existing browser session + pageFilter: 'readability', // Use readability for webpage processing + + // Custom settings + customPrompt: + 'Always prioritize readability and simplicity in your code. Prefer TypeScript over JavaScript when possible.', + tokenCache: false, // Disable token caching for LLM API calls -# Use existing browser session -mycoder config set userSession true + // Other configuration options... +}; +``` -# Use readability for webpage processing -mycoder config set pageFilter readability +You can also set these options via CLI arguments (which will override the config file): -# Set custom instructions for the agent -mycoder config set customPrompt "Always prioritize readability and simplicity in your code. Prefer TypeScript over JavaScript when possible." +```bash +# Set browser to show UI for this session only +mycoder --headless false "Your prompt here" -# Disable token caching for LLM API calls -mycoder config set tokenCache false +# Use existing browser session for this session only +mycoder --userSession true "Your prompt here" ``` ## Environment Variables diff --git a/packages/cli/package.json b/packages/cli/package.json index 402c04f..6f7a6c9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "mycoder", "description": "A command line tool using agent that can do arbitrary tasks, including coding tasks", - "version": "0.11.0", + "version": "1.0.1", "type": "module", "bin": "./bin/cli.js", "main": "./dist/index.js", @@ -27,10 +27,7 @@ "test": "vitest run", "test:watch": "vitest", "test:ci": "vitest --run --coverage", - "changeset": "changeset", - "version": "changeset version", - "prepublishOnly": "pnpm run clean && pnpm run build && pnpm run test", - "semantic-release": "semantic-release -e semantic-release-monorepo" + "semantic-release": "pnpm exec semantic-release -e semantic-release-monorepo" }, "keywords": [ "ai", @@ -50,6 +47,7 @@ "dependencies": { "@sentry/node": "^9.3.0", "chalk": "^5", + "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.1", "dotenv": "^16", "mycoder-agent": "workspace:*", diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index 1bc50e2..cc57b93 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -1,5 +1,4 @@ import * as fs from 'fs/promises'; -import { createInterface } from 'readline/promises'; import chalk from 'chalk'; import { @@ -12,15 +11,16 @@ import { LogLevel, subAgentTool, errorToString, - getModel, DEFAULT_CONFIG, + AgentConfig, + ModelProvider, } from 'mycoder-agent'; import { TokenTracker } from 'mycoder-agent/dist/core/tokens.js'; import { SharedOptions } from '../options.js'; -import { initSentry, captureException } from '../sentry/index.js'; -import { getConfig } from '../settings/config.js'; -import { hasUserConsented, saveUserConsent } from '../settings/settings.js'; +import { captureException } from '../sentry/index.js'; +import { getConfigFromArgv, loadConfig } from '../settings/config.js'; +import { checkGitCli } from '../utils/gitCliCheck.js'; import { nameToLogIndex } from '../utils/nameToLogIndex.js'; import { checkForUpdates, getPackageInfo } from '../utils/versionCheck.js'; @@ -40,104 +40,109 @@ export const command: CommandModule = { }) as Argv; }, handler: async (argv) => { - // Initialize Sentry with custom DSN if provided - if (argv.sentryDsn) { - initSentry(argv.sentryDsn); - } + const packageInfo = getPackageInfo(); + + // Get configuration for model provider and name + const config = await loadConfig(getConfigFromArgv(argv)); const logger = new Logger({ name: 'Default', - logLevel: nameToLogIndex(argv.logLevel), + logLevel: nameToLogIndex(config.logLevel), customPrefix: subAgentTool.logPrefix, }); - const packageInfo = getPackageInfo(); - logger.info( `MyCoder v${packageInfo.version} - AI-powered coding assistant`, ); // Skip version check if upgradeCheck is false - if (argv.upgradeCheck !== false) { + if (config.upgradeCheck !== false) { await checkForUpdates(logger); } - // Skip user consent check if userWarning is false - if (!hasUserConsented() && argv.userWarning !== false) { - const readline = createInterface({ - input: process.stdin, - output: process.stdout, - }); - - logger.warn( - 'This tool can do anything on your command line that you ask it to.', - 'It can delete files, install software, and even send data to remote servers.', - 'It is a powerful tool that should be used with caution.', - 'Do you consent to using this tool at your own risk? (y/N)', + // Check for git and gh CLI tools if GitHub mode is enabled + if (config.githubMode) { + logger.debug( + 'GitHub mode is enabled, checking for git and gh CLI tools...', ); + const gitCliCheck = await checkGitCli(logger); - const answer = (await readline.question('> ')).trim().toLowerCase(); - readline.close(); - - if (answer === 'y' || answer === 'yes') { - saveUserConsent(); + if (gitCliCheck.errors.length > 0) { + logger.warn( + 'GitHub mode is enabled but there are issues with git/gh CLI tools:', + ); + gitCliCheck.errors.forEach((error) => logger.warn(`- ${error}`)); + + if (!gitCliCheck.gitAvailable || !gitCliCheck.ghAvailable) { + logger.warn( + 'GitHub mode requires git and gh CLI tools to be installed.', + ); + logger.warn( + 'Please install the missing tools or disable GitHub mode with --githubMode false', + ); + // Disable GitHub mode if git or gh CLI is not available + logger.info('Disabling GitHub mode due to missing CLI tools.'); + config.githubMode = false; + } else if (!gitCliCheck.ghAuthenticated) { + logger.warn( + 'GitHub CLI is not authenticated. Please run "gh auth login" to authenticate.', + ); + // Disable GitHub mode if gh CLI is not authenticated + logger.info( + 'Disabling GitHub mode due to unauthenticated GitHub CLI.', + ); + config.githubMode = false; + } } else { - logger.info('User did not consent. Exiting.'); - throw new Error('User did not consent'); + logger.info( + 'GitHub mode is enabled and all required CLI tools are available.', + ); } - } else if (!hasUserConsented() && argv.userWarning === false) { - // Just skip the consent check without saving consent when userWarning is false - logger.debug('Skipping user consent check due to --userWarning=false'); - // Note: We don't save consent here, just bypassing the check for this session } const tokenTracker = new TokenTracker( 'Root', undefined, - argv.tokenUsage ? LogLevel.info : LogLevel.debug, + config.tokenUsage ? LogLevel.info : LogLevel.debug, ); + // Use command line option if provided, otherwise use config value + tokenTracker.tokenCache = config.tokenCache; try { - // Get configuration for model provider and name - const userConfig = getConfig(); - // Use command line option if provided, otherwise use config value - tokenTracker.tokenCache = - argv.tokenCache !== undefined ? argv.tokenCache : userConfig.tokenCache; - - const userModelProvider = argv.provider || userConfig.provider; - const userModelName = argv.model || userConfig.model; - const userMaxTokens = argv.maxTokens || userConfig.maxTokens; - const userTemperature = argv.temperature || userConfig.temperature; - // Early API key check based on model provider const providerSettings = - providerConfig[userModelProvider as keyof typeof providerConfig]; + providerConfig[config.provider as keyof typeof providerConfig]; if (providerSettings) { const { keyName } = providerSettings; // First check if the API key is in the config - const configApiKey = userConfig[ - keyName as keyof typeof userConfig - ] as string; + const configApiKey = config[keyName as keyof typeof config] as string; // Then fall back to environment variable const envApiKey = process.env[keyName]; // Use config key if available, otherwise use env key const apiKey = configApiKey || envApiKey; if (!apiKey) { - logger.error(getProviderApiKeyError(userModelProvider)); - throw new Error(`${userModelProvider} API key not found`); + logger.error(getProviderApiKeyError(config.provider)); + throw new Error(`${config.provider} API key not found`); } // If we're using a key from config, set it as an environment variable // This ensures it's available to the provider libraries if (configApiKey && !envApiKey) { process.env[keyName] = configApiKey; - logger.debug(`Using ${keyName} from configuration`); + logger.info(`Using ${keyName} from configuration`); } + } else if (config.provider === 'ollama') { + // For Ollama, we check if the base URL is set + const ollamaBaseUrl = argv.ollamaBaseUrl || config.ollamaBaseUrl; + logger.info(`Using Ollama with base URL: ${ollamaBaseUrl}`); + } else { + // Unknown provider + logger.info(`Unknown provider: ${config.provider}`); + throw new Error(`Unknown provider: ${config.provider}`); } - // No API key check needed for Ollama as it uses a local server let prompt: string | undefined; @@ -171,12 +176,7 @@ export const command: CommandModule = { ].join('\n'); const tools = getTools({ - enableUserPrompt: - argv.userPrompt !== undefined - ? argv.userPrompt - : argv.enableUserPrompt !== undefined - ? argv.enableUserPrompt - : true, + userPrompt: config.userPrompt, }); // Error handling @@ -187,40 +187,27 @@ export const command: CommandModule = { ); process.exit(0); }); - const config = await getConfig(); - // Create a config with the selected model - const agentConfig = { + // Create a config for the agent + const agentConfig: AgentConfig = { ...DEFAULT_CONFIG, - model: getModel( - userModelProvider as 'anthropic' /* - | 'openai' - | 'ollama' - | 'xai' - | 'mistral'*/, - userModelName, - ), - maxTokens: userMaxTokens, - temperature: userTemperature, }; const result = await toolAgent(prompt, tools, agentConfig, { logger, - headless: argv.headless ?? config.headless, - userSession: argv.userSession ?? config.userSession, - pageFilter: argv.pageFilter ?? config.pageFilter, + headless: config.headless, + userSession: config.userSession, + pageFilter: config.pageFilter, workingDirectory: '.', tokenTracker, - githubMode: argv.githubMode ?? config.githubMode, + githubMode: config.githubMode, customPrompt: config.customPrompt, - tokenCache: - argv.tokenCache !== undefined ? argv.tokenCache : config.tokenCache, - enableUserPrompt: - argv.userPrompt !== undefined - ? argv.userPrompt - : argv.enableUserPrompt !== undefined - ? argv.enableUserPrompt - : true, + tokenCache: config.tokenCache, + userPrompt: config.userPrompt, + provider: config.provider as ModelProvider, + model: config.model, + maxTokens: config.maxTokens, + temperature: config.temperature, }); const output = diff --git a/packages/cli/src/commands/config.ts b/packages/cli/src/commands/config.ts deleted file mode 100644 index ff463c2..0000000 --- a/packages/cli/src/commands/config.ts +++ /dev/null @@ -1,327 +0,0 @@ -import chalk from 'chalk'; -import { Logger } from 'mycoder-agent'; - -import { SharedOptions } from '../options.js'; -import { - getConfig, - getDefaultConfig, - updateConfig, - getConfigAtLevel, - clearConfigAtLevel, - ConfigLevel, -} from '../settings/config.js'; -import { nameToLogIndex } from '../utils/nameToLogIndex.js'; - -import type { CommandModule, ArgumentsCamelCase } from 'yargs'; - -export interface ConfigOptions extends SharedOptions { - command: 'get' | 'set' | 'list' | 'clear'; - key?: string; - value?: string; - all?: boolean; -} - -export const command: CommandModule = { - command: 'config [key] [value]', - describe: 'Manage MyCoder configuration', - builder: (yargs) => { - return yargs - .positional('command', { - describe: 'Config command to run', - choices: ['get', 'set', 'list', 'clear'], - type: 'string', - demandOption: true, - }) - .positional('key', { - describe: 'Configuration key', - type: 'string', - }) - .positional('value', { - describe: 'Configuration value (for set command)', - type: 'string', - }) - .option('all', { - describe: 'Clear all configuration settings (for clear command)', - type: 'boolean', - default: false, - }) - .example('$0 config list', 'List all configuration values') - .example( - '$0 config get githubMode', - 'Get the value of githubMode setting', - ) - .example('$0 config set githubMode true', 'Enable GitHub mode') - .example( - '$0 config clear customPrompt', - 'Reset customPrompt to default value', - ) - .example( - '$0 config set ANTHROPIC_API_KEY ', - 'Store your Anthropic API key in configuration', - ) - .example( - '$0 config clear --all', - 'Clear all configuration settings', - ) as any; // eslint-disable-line @typescript-eslint/no-explicit-any - }, - handler: async (argv: ArgumentsCamelCase) => { - const logger = new Logger({ - name: 'Config', - logLevel: nameToLogIndex(argv.logLevel), - }); - - // Determine which config level to use based on flags - const configLevel = - argv.global || argv.g ? ConfigLevel.GLOBAL : ConfigLevel.PROJECT; - const levelName = configLevel === ConfigLevel.GLOBAL ? 'global' : 'project'; - - // Check if project level is writable when needed for operations that write to config - if ( - configLevel === ConfigLevel.PROJECT && - (argv.command === 'set' || - (argv.command === 'clear' && (argv.key || argv.all))) - ) { - try { - // Import directly to avoid circular dependency - const { isProjectSettingsDirWritable } = await import( - '../settings/settings.js' - ); - if (!isProjectSettingsDirWritable()) { - logger.error( - chalk.red( - 'Cannot write to project configuration directory. Check permissions or use --global flag.', - ), - ); - logger.info( - 'You can use the --global (-g) flag to modify global configuration instead.', - ); - return; - } - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : String(error); - logger.error( - chalk.red( - `Error checking project directory permissions: ${errorMessage}`, - ), - ); - return; - } - } - - // Get merged config for display - const config = getConfig(); - - // Handle 'list' command - if (argv.command === 'list') { - logger.info('Current configuration:'); - const defaultConfig = getDefaultConfig(); - - // Get all valid config keys - const validKeys = Object.keys(defaultConfig); - - // Filter and sort config entries - const configEntries = Object.entries(config) - .filter(([key]) => validKeys.includes(key)) - .sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); - - // Display config entries with default indicators - configEntries.forEach(([key, value]) => { - const isDefault = - JSON.stringify(value) === - JSON.stringify(defaultConfig[key as keyof typeof defaultConfig]); - const valueDisplay = isDefault - ? chalk.dim(`${value} (default)`) - : chalk.green(value); - logger.info(` ${key}: ${valueDisplay}`); - }); - return; - } - - // Handle 'get' command - if (argv.command === 'get') { - if (!argv.key) { - logger.error('Key is required for get command'); - return; - } - - if (argv.key in config) { - logger.info( - `${argv.key}: ${chalk.green(config[argv.key as keyof typeof config])}`, - ); - } else { - logger.error(`Configuration key '${argv.key}' not found`); - } - return; - } - - // Handle 'set' command - if (argv.command === 'set') { - if (!argv.key) { - logger.error('Key is required for set command'); - return; - } - - if (argv.value === undefined) { - logger.error('Value is required for set command'); - return; - } - - // Check if the key exists in default config - const defaultConfig = getDefaultConfig(); - if (!(argv.key in defaultConfig)) { - logger.warn( - `Warning: '${argv.key}' is not a standard configuration key`, - ); - logger.info( - `Valid configuration keys: ${Object.keys(defaultConfig).join(', ')}`, - ); - // Continue with the operation instead of returning - } - - // Parse the value based on current type or infer boolean/number - let parsedValue: string | boolean | number = argv.value; - - // Check if config already exists to determine type - if (argv.key in config) { - if (typeof config[argv.key as keyof typeof config] === 'boolean') { - parsedValue = argv.value.toLowerCase() === 'true'; - } else if ( - typeof config[argv.key as keyof typeof config] === 'number' - ) { - parsedValue = Number(argv.value); - } - } else { - // If config doesn't exist yet, try to infer type - if ( - argv.value.toLowerCase() === 'true' || - argv.value.toLowerCase() === 'false' - ) { - parsedValue = argv.value.toLowerCase() === 'true'; - } else if (!isNaN(Number(argv.value))) { - parsedValue = Number(argv.value); - } - } - - try { - // Update config at the specified level - const updatedConfig = updateConfig( - { [argv.key]: parsedValue }, - configLevel, - ); - - logger.info( - `Updated ${argv.key}: ${chalk.green(updatedConfig[argv.key as keyof typeof updatedConfig])} at ${levelName} level`, - ); - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : String(error); - logger.error( - chalk.red(`Failed to update configuration: ${errorMessage}`), - ); - if (configLevel === ConfigLevel.PROJECT) { - logger.info( - 'You can use the --global (-g) flag to modify global configuration instead.', - ); - } - } - return; - } - - // Handle 'clear' command - if (argv.command === 'clear') { - // Check if --all flag is provided - if (argv.all) { - try { - // Clear settings at the specified level - clearConfigAtLevel(configLevel); - logger.info( - `All ${levelName} configuration settings have been cleared.`, - ); - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : String(error); - logger.error( - chalk.red(`Failed to clear configuration: ${errorMessage}`), - ); - if (configLevel === ConfigLevel.PROJECT) { - logger.info( - 'You can use the --global (-g) flag to modify global configuration instead.', - ); - } - } - return; - } - - if (!argv.key) { - logger.error( - 'Key is required for clear command (or use --all to clear all settings)', - ); - return; - } - - const defaultConfig = getDefaultConfig(); - - // Check if the key exists in the config - if (!(argv.key in config)) { - logger.error(`Configuration key '${argv.key}' not found`); - return; - } - - // Check if the key exists in the default config - if (!(argv.key in defaultConfig)) { - logger.error( - `Configuration key '${argv.key}' does not have a default value`, - ); - return; - } - - // Get the current config, create a new object without the specified key - const currentConfig = getConfig(); - const { [argv.key]: _, ...newConfig } = currentConfig as Record< - string, - any - >; - - // Update the config file with the new object - updateConfig(newConfig); - - // Get the default value that will now be used - const defaultValue = - defaultConfig[argv.key as keyof typeof defaultConfig]; - - // Get the effective config after clearing - const updatedConfig = getConfig(); - const newValue = updatedConfig[argv.key as keyof typeof updatedConfig]; - - // Determine where the new value is coming from - const isDefaultAfterClear = - JSON.stringify(newValue) === JSON.stringify(defaultValue); - const afterClearInGlobal = - !isDefaultAfterClear && - argv.key in getConfigAtLevel(ConfigLevel.GLOBAL); - const afterClearInProject = - !isDefaultAfterClear && - !afterClearInGlobal && - argv.key in getConfigAtLevel(ConfigLevel.PROJECT); - - let sourceDisplay = ''; - if (isDefaultAfterClear) { - sourceDisplay = '(default)'; - } else if (afterClearInProject) { - sourceDisplay = '(from project config)'; - } else if (afterClearInGlobal) { - sourceDisplay = '(from global config)'; - } - - logger.info( - `Cleared ${argv.key} at ${levelName} level, now using: ${chalk.green(newValue)} ${sourceDisplay}`, - ); - return; - } - - // If command not recognized - logger.error(`Unknown config command: ${argv.command}`); - logger.info('Available commands: get, set, list, clear'); - }, -}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ebda1b6..ffbabf2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -2,17 +2,17 @@ import { createRequire } from 'module'; import * as dotenv from 'dotenv'; import sourceMapSupport from 'source-map-support'; -import yargs, { CommandModule } from 'yargs'; +import yargs, { ArgumentsCamelCase, CommandModule } from 'yargs'; import { hideBin } from 'yargs/helpers'; import { command as defaultCommand } from './commands/$default.js'; -import { command as configCommand } from './commands/config.js'; import { command as testProfileCommand } from './commands/test-profile.js'; import { command as testSentryCommand } from './commands/test-sentry.js'; import { command as toolsCommand } from './commands/tools.js'; -import { sharedOptions } from './options.js'; +import { SharedOptions, sharedOptions } from './options.js'; import { initSentry, captureException } from './sentry/index.js'; -import { getConfig } from './settings/config.js'; +import { getConfigFromArgv, loadConfig } from './settings/config.js'; +import { cleanupResources, setupForceExit } from './utils/cleanup.js'; import { enableProfiling, mark, reportTimings } from './utils/performance.js'; mark('After imports'); @@ -58,17 +58,18 @@ const main = async () => { testSentryCommand, testProfileCommand, toolsCommand, - configCommand, ] as CommandModule[]) .strict() .showHelpOnFail(true) .help().argv; // Get config to check for profile setting - const config = getConfig(); + const config = await loadConfig( + getConfigFromArgv(argv as ArgumentsCamelCase), + ); // Enable profiling if --profile flag is set or if enabled in config - enableProfiling(Boolean(argv.profile) || Boolean(config.profile)); + enableProfiling(config.profile); mark('After yargs setup'); }; @@ -82,4 +83,11 @@ await main() .finally(async () => { // Report timings if profiling is enabled await reportTimings(); + + // Clean up all resources before exit + await cleanupResources(); + + // Setup a force exit as a failsafe + // This ensures the process will exit even if there are lingering handles + setupForceExit(5000); }); diff --git a/packages/cli/src/options.ts b/packages/cli/src/options.ts index 5de958d..94d2994 100644 --- a/packages/cli/src/options.ts +++ b/packages/cli/src/options.ts @@ -13,11 +13,11 @@ export type SharedOptions = { readonly temperature?: number; readonly profile?: boolean; readonly tokenCache?: boolean; - readonly enableUserPrompt?: boolean; readonly userPrompt?: boolean; readonly githubMode?: boolean; readonly userWarning?: boolean; readonly upgradeCheck?: boolean; + readonly ollamaBaseUrl?: string; }; export const sharedOptions = { @@ -25,18 +25,17 @@ export const sharedOptions = { type: 'string', alias: 'l', description: 'Set minimum logging level', - default: 'info', + choices: ['debug', 'verbose', 'info', 'warn', 'error'], } as const, profile: { type: 'boolean', description: 'Enable performance profiling of CLI startup', - default: false, } as const, provider: { type: 'string', description: 'AI model provider to use', - choices: ['anthropic' /*, 'openai', 'ollama', 'xai', 'mistral'*/], + choices: ['anthropic', 'ollama' /*, 'openai', 'xai', 'mistral'*/], } as const, model: { type: 'string', @@ -64,60 +63,41 @@ export const sharedOptions = { tokenUsage: { type: 'boolean', description: 'Output token usage at info log level', - default: false, } as const, headless: { type: 'boolean', description: 'Use browser in headless mode with no UI showing', - default: true, } as const, userSession: { type: 'boolean', description: "Use user's existing browser session instead of sandboxed session", - default: false, } as const, pageFilter: { type: 'string', description: 'Method to process webpage content', - default: 'none', choices: ['simple', 'none', 'readability'], } as const, - sentryDsn: { - type: 'string', - description: 'Custom Sentry DSN for error tracking', - hidden: true, - } as const, tokenCache: { type: 'boolean', description: 'Enable token caching for LLM API calls', } as const, - enableUserPrompt: { - type: 'boolean', - description: - 'Enable or disable the userPrompt tool (disable for fully automated sessions)', - default: true, - } as const, userPrompt: { type: 'boolean', - description: - 'Alias for enableUserPrompt: enable or disable the userPrompt tool', - default: true, + description: 'Alias for userPrompt: enable or disable the userPrompt tool', } as const, githubMode: { - type: 'boolean', - description: 'Enable GitHub mode for working with issues and PRs', - default: false, - } as const, - userWarning: { type: 'boolean', description: - 'Skip user consent check for current session (does not save consent)', - default: false, + 'Enable GitHub mode for working with issues and PRs (requires git and gh CLI tools)', + default: true, } as const, upgradeCheck: { type: 'boolean', description: 'Disable version upgrade check (for automated/remote usage)', - default: false, + } as const, + ollamaBaseUrl: { + type: 'string', + description: 'Base URL for Ollama API (default: http://localhost:11434)', } as const, }; diff --git a/packages/cli/src/settings/config.test.ts b/packages/cli/src/settings/config.test.ts deleted file mode 100644 index 789c08a..0000000 --- a/packages/cli/src/settings/config.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -// Mock modules -vi.mock('fs', () => ({ - existsSync: vi.fn(), - readFileSync: vi.fn(), - writeFileSync: vi.fn(), - unlinkSync: vi.fn(), -})); - -vi.mock('path', () => ({ - join: vi.fn(), -})); - -// Mock settings module -vi.mock('./settings.js', () => ({ - getSettingsDir: vi.fn().mockReturnValue('/test/home/dir/.mycoder'), - getProjectSettingsDir: vi.fn().mockReturnValue('/test/project/dir/.mycoder'), - isProjectSettingsDirWritable: vi.fn().mockReturnValue(true), -})); - -// Import after mocking -import { readConfigFile } from './config.js'; - -describe('Hierarchical Configuration', () => { - // Mock file paths - const mockGlobalConfigPath = '/test/home/dir/.mycoder/config.json'; - const mockProjectConfigPath = '/test/project/dir/.mycoder/config.json'; - - // Mock config data - const mockGlobalConfig = { - provider: 'openai', - model: 'gpt-4', - }; - - const mockProjectConfig = { - model: 'claude-3-opus', - }; - - beforeEach(() => { - vi.resetAllMocks(); - - // Set environment - process.env.VITEST = 'true'; - - // Mock path.join - vi.mocked(path.join).mockImplementation((...args) => { - if (args.includes('/test/home/dir/.mycoder')) { - return mockGlobalConfigPath; - } - if (args.includes('/test/project/dir/.mycoder')) { - return mockProjectConfigPath; - } - return args.join('/'); - }); - - // Mock fs.existsSync - vi.mocked(fs.existsSync).mockReturnValue(true); - - // Mock fs.readFileSync - vi.mocked(fs.readFileSync).mockImplementation((filePath) => { - if (filePath === mockGlobalConfigPath) { - return JSON.stringify(mockGlobalConfig); - } - if (filePath === mockProjectConfigPath) { - return JSON.stringify(mockProjectConfig); - } - return ''; - }); - }); - - // Only test the core function that's actually testable - it('should read config files correctly', () => { - const globalConfig = readConfigFile(mockGlobalConfigPath); - expect(globalConfig).toEqual(mockGlobalConfig); - - const projectConfig = readConfigFile(mockProjectConfigPath); - expect(projectConfig).toEqual(mockProjectConfig); - }); -}); diff --git a/packages/cli/src/settings/config.ts b/packages/cli/src/settings/config.ts index 159954b..74c4b88 100644 --- a/packages/cli/src/settings/config.ts +++ b/packages/cli/src/settings/config.ts @@ -1,300 +1,106 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -import deepmerge from 'deepmerge'; - -import { - getSettingsDir, - getProjectSettingsDir, - isProjectSettingsDirWritable, -} from './settings.js'; - -// Configuration levels enum -export enum ConfigLevel { - DEFAULT = 'default', - GLOBAL = 'global', - PROJECT = 'project', - CLI = 'cli', -} - -// File paths for different config levels -const globalConfigFile = path.join(getSettingsDir(), 'config.json'); - -// Export for testing -export const getProjectConfigFile = (): string => { - const projectDir = getProjectSettingsDir(); - return projectDir ? path.join(projectDir, 'config.json') : ''; +import { cosmiconfig } from 'cosmiconfig'; +import { ArgumentsCamelCase } from 'yargs'; + +import { SharedOptions } from '../options'; + +export type Config = { + logLevel: string; + githubMode: boolean; + headless: boolean; + userSession: boolean; + pageFilter: 'simple' | 'none' | 'readability'; + provider: string; + model: string; + maxTokens: number; + temperature: number; + customPrompt: string; + profile: boolean; + tokenCache: boolean; + userPrompt: boolean; + upgradeCheck: boolean; + tokenUsage: boolean; + + ollamaBaseUrl: string; }; -// For internal use - use the function directly to ensure it's properly mocked in tests -const projectConfigFile = (): string => getProjectConfigFile(); - // Default configuration -const defaultConfig = { - // Add default configuration values here - githubMode: false, +const defaultConfig: Config = { + logLevel: 'info', + + // GitHub integration + githubMode: true, + + // Browser settings headless: true, userSession: false, pageFilter: 'none' as 'simple' | 'none' | 'readability', + + // Model settings provider: 'anthropic', model: 'claude-3-7-sonnet-20250219', maxTokens: 4096, temperature: 0.7, + + // Custom settings customPrompt: '', profile: false, tokenCache: true, - // API keys (empty by default) - ANTHROPIC_API_KEY: '', -}; - -export type Config = typeof defaultConfig; - -// Export the default config for use in other functions -export const getDefaultConfig = (): Config => { - return { ...defaultConfig }; -}; + userPrompt: true, + upgradeCheck: true, + tokenUsage: false, -/** - * Read a config file from disk - * @param filePath Path to the config file - * @returns The config object or an empty object if the file doesn't exist or is invalid - */ -export const readConfigFile = (filePath: string): Partial => { - if (!filePath || !fs.existsSync(filePath)) { - return {}; - } - try { - const fileContent = fs.readFileSync(filePath, 'utf-8'); - return JSON.parse(fileContent); - } catch { - return defaultConfig; - } + // Ollama configuration + ollamaBaseUrl: 'http://localhost:11434', }; -/** - * Get configuration from a specific level - * @param level The configuration level to retrieve - * @returns The configuration at the specified level - */ -export const getConfigAtLevel = (level: ConfigLevel): Partial => { - let configFile: string; - - switch (level) { - case ConfigLevel.DEFAULT: - return getDefaultConfig(); - case ConfigLevel.GLOBAL: - configFile = globalConfigFile; - return readConfigFile(configFile); - case ConfigLevel.PROJECT: - configFile = projectConfigFile(); - return configFile ? readConfigFile(configFile) : {}; - case ConfigLevel.CLI: - return {}; // CLI options are passed directly from the command - default: - return {}; - } +export const getConfigFromArgv = (argv: ArgumentsCamelCase) => { + return { + logLevel: argv.logLevel, + tokenCache: argv.tokenCache, + provider: argv.provider, + model: argv.model, + maxTokens: argv.maxTokens, + temperature: argv.temperature, + profile: argv.profile, + githubMode: argv.githubMode, + userSession: argv.userSession, + pageFilter: argv.pageFilter, + headless: argv.headless, + ollamaBaseUrl: argv.ollamaBaseUrl, + userPrompt: argv.userPrompt, + upgradeCheck: argv.upgradeCheck, + tokenUsage: argv.tokenUsage, + }; }; +function removeUndefined(obj: any) { + return Object.fromEntries( + Object.entries(obj).filter(([_, value]) => value !== undefined), + ); +} /** - * Get the merged configuration from all levels - * @param cliOptions Optional CLI options to include in the merge - * @returns The merged configuration with all levels applied - */ -export const getConfig = (cliOptions: Partial = {}): Config => { - // Start with default config - const defaultConf = getDefaultConfig(); - - // Read global config - const globalConf = getConfigAtLevel(ConfigLevel.GLOBAL); - - // Read project config - const projectConf = getConfigAtLevel(ConfigLevel.PROJECT); - - // For tests, use a simpler merge approach when testing - if (process.env.VITEST) { - return { - ...defaultConf, - ...globalConf, - ...projectConf, - ...cliOptions, - } as Config; - } - - // Merge in order of precedence: default < global < project < cli - return deepmerge.all([ - defaultConf, - globalConf, - projectConf, - cliOptions, - ]) as Config; -}; - -/** - * Update configuration at a specific level - * @param config Configuration changes to apply - * @param level The level at which to apply the changes - * @returns The new merged configuration after the update - */ -export const updateConfig = ( - config: Partial, - level: ConfigLevel = ConfigLevel.PROJECT, -): Config => { - let targetFile: string; - - // Determine which file to update - switch (level) { - case ConfigLevel.GLOBAL: - targetFile = globalConfigFile; - break; - case ConfigLevel.PROJECT: - // Check if project config directory is writable - if (!isProjectSettingsDirWritable()) { - throw new Error( - 'Cannot write to project configuration directory. Check permissions or use --global flag.', - ); - } - targetFile = projectConfigFile(); - if (!targetFile) { - throw new Error( - 'Cannot determine project configuration file path. Use --global flag instead.', - ); - } - break; - default: - throw new Error(`Cannot update configuration at level: ${level}`); - } - - // Read current config at the target level - const currentLevelConfig = readConfigFile(targetFile); - - // Merge the update with the current config at this level - const updatedLevelConfig = { ...currentLevelConfig, ...config }; - - // Write the updated config back to the file - try { - fs.writeFileSync(targetFile, JSON.stringify(updatedLevelConfig, null, 2)); - } catch (error) { - console.error(`Error writing to ${targetFile}:`, error); - throw error; - } - - // For tests, return just the updated level config when in test environment - if (process.env.NODE_ENV === 'test' || process.env.VITEST) { - // For tests, return just the config that was passed in - return config as Config; - } - - // Return the new merged configuration - return getConfig(); -}; - -/** - * Clears configuration settings at a specific level - * @param level The level at which to clear settings - * @returns The new merged configuration after clearing - */ -export const clearConfigAtLevel = (level: ConfigLevel): Config => { - let targetFile: string; - - // Determine which file to clear - switch (level) { - case ConfigLevel.GLOBAL: - targetFile = globalConfigFile; - break; - case ConfigLevel.PROJECT: - // Check if project config directory is writable - if (!isProjectSettingsDirWritable()) { - throw new Error( - 'Cannot write to project configuration directory. Check permissions or use --global flag.', - ); - } - targetFile = projectConfigFile(); - if (!targetFile) { - // If no project config file exists, nothing to clear - return getConfig(); - } - break; - default: - throw new Error(`Cannot clear configuration at level: ${level}`); - } - - // Remove the config file if it exists - if (fs.existsSync(targetFile)) { - fs.unlinkSync(targetFile); - } - - // For tests, return empty config - if (process.env.VITEST) { - return getDefaultConfig(); - } - - // Return the new merged configuration - return getConfig(); -}; - -/** - * Clears a specific key from configuration at a specific level - * @param key The key to clear - * @param level The level from which to clear the key - * @returns The new merged configuration after clearing - */ -export const clearConfigKey = ( - key: string, - level: ConfigLevel = ConfigLevel.PROJECT, -): Config => { - let targetFile: string; - - // Determine which file to update - switch (level) { - case ConfigLevel.GLOBAL: - targetFile = globalConfigFile; - break; - case ConfigLevel.PROJECT: - // Check if project config directory is writable - if (!isProjectSettingsDirWritable()) { - throw new Error( - 'Cannot write to project configuration directory. Check permissions or use --global flag.', - ); - } - targetFile = projectConfigFile(); - if (!targetFile) { - // If no project config file exists, nothing to clear - return getConfig(); - } - break; - default: - throw new Error(`Cannot clear key at configuration level: ${level}`); - } - - // Read current config at the target level - const currentLevelConfig = readConfigFile(targetFile); - - // Skip if the key doesn't exist - if (!(key in currentLevelConfig)) { - return getConfig(); - } - - // Create a new config without the specified key - const { [key]: _, ...newConfig } = currentLevelConfig as Record; - - // Write the updated config back to the file - fs.writeFileSync(targetFile, JSON.stringify(newConfig, null, 2)); - - // Return the new merged configuration - return getConfig(); -}; - -/** - * For backwards compatibility - clears all configuration - * @returns The default configuration that will now be used + * Load configuration using cosmiconfig + * @returns Merged configuration with default values */ -export const clearAllConfig = (): Config => { - // Clear both global and project configs for backwards compatibility - clearConfigAtLevel(ConfigLevel.GLOBAL); - try { - clearConfigAtLevel(ConfigLevel.PROJECT); - } catch { - // Ignore errors when clearing project config - } - return getDefaultConfig(); -}; +export async function loadConfig( + cliOptions: Partial = {}, +): Promise { + // Initialize cosmiconfig + const explorer = cosmiconfig('mycoder', { + searchStrategy: 'global', + }); + + // Search for configuration file + const result = await explorer.search(); + + // Merge configurations with precedence: default < file < cli + const fileConfig = result?.config || {}; + + // Return merged configuration + const mergedConfig = { + ...defaultConfig, + ...removeUndefined(fileConfig), + ...removeUndefined(cliOptions), + }; + return mergedConfig; +} diff --git a/packages/cli/src/settings/settings.ts b/packages/cli/src/settings/settings.ts index fb07544..3721fa7 100644 --- a/packages/cli/src/settings/settings.ts +++ b/packages/cli/src/settings/settings.ts @@ -10,69 +10,3 @@ export const getSettingsDir = (): string => { } return settingsDir; }; - -/** - * Gets the project-level settings directory - * @returns The project settings directory path, or empty string if not in a project - */ -export const getProjectSettingsDir = (): string => { - // Start with the current directory - let currentDir = process.cwd(); - - // Traverse up the directory tree until we find a .mycoder directory or reach the root - while (currentDir !== path.parse(currentDir).root) { - const projectSettingsDir = path.join(currentDir, '.mycoder'); - if ( - fs.existsSync(projectSettingsDir) && - fs.statSync(projectSettingsDir).isDirectory() - ) { - return projectSettingsDir; - } - // Move up one directory - currentDir = path.dirname(currentDir); - } - - // If we're creating a new project config, use the current directory - return path.join(process.cwd(), '.mycoder'); -}; - -/** - * Checks if the project settings directory is writable - * @returns True if the directory exists and is writable, or can be created - */ -export const isProjectSettingsDirWritable = (): boolean => { - const projectDir = getProjectSettingsDir(); - - // Check if directory exists - if (fs.existsSync(projectDir)) { - try { - // Try to write a test file to check permissions - const testFile = path.join(projectDir, '.write-test'); - fs.writeFileSync(testFile, ''); - fs.unlinkSync(testFile); - return true; - } catch { - return false; - } - } else { - // Directory doesn't exist yet, check if we can create it - try { - fs.mkdirSync(projectDir, { recursive: true }); - return true; - } catch { - return false; - } - } -}; - -const consentFile = path.join(settingsDir, 'consent.json'); - -export const hasUserConsented = (): boolean => { - return fs.existsSync(consentFile); -}; - -export const saveUserConsent = (): void => { - const timestamp = new Date().toISOString(); - const data = JSON.stringify({ timestamp }, null, 2); - fs.writeFileSync(consentFile, data); -}; diff --git a/packages/cli/src/utils/cleanup.ts b/packages/cli/src/utils/cleanup.ts new file mode 100644 index 0000000..b3fa6e8 --- /dev/null +++ b/packages/cli/src/utils/cleanup.ts @@ -0,0 +1,69 @@ +import { BrowserManager, processStates } from 'mycoder-agent'; + +/** + * Handles cleanup of resources before application exit + * Ensures all browser sessions and shell processes are terminated + */ +export async function cleanupResources(): Promise { + console.log('Cleaning up resources before exit...'); + + // 1. Clean up browser sessions + try { + // Get the BrowserManager instance - this is a singleton + const browserManager = (globalThis as any).__BROWSER_MANAGER__ as + | BrowserManager + | undefined; + if (browserManager) { + console.log('Closing all browser sessions...'); + await browserManager.closeAllSessions(); + } + } catch (error) { + console.error('Error closing browser sessions:', error); + } + + // 2. Clean up shell processes + try { + if (processStates.size > 0) { + console.log(`Terminating ${processStates.size} shell processes...`); + for (const [id, state] of processStates.entries()) { + if (!state.state.completed) { + console.log(`Terminating process ${id}...`); + try { + state.process.kill('SIGTERM'); + // Force kill after a short timeout if still running + setTimeout(() => { + try { + if (!state.state.completed) { + state.process.kill('SIGKILL'); + } + // eslint-disable-next-line unused-imports/no-unused-vars + } catch (e) { + // Ignore errors on forced kill + } + }, 500); + } catch (e) { + console.error(`Error terminating process ${id}:`, e); + } + } + } + } + } catch (error) { + console.error('Error terminating shell processes:', error); + } + + // 3. Give async operations a moment to complete + await new Promise((resolve) => setTimeout(resolve, 1000)); + + console.log('Cleanup completed'); +} + +/** + * Force exits the process after a timeout + * This is a failsafe to ensure the process exits even if there are lingering handles + */ +export function setupForceExit(timeoutMs = 5000): void { + setTimeout(() => { + console.log(`Forcing exit after ${timeoutMs}ms timeout`); + process.exit(0); + }, timeoutMs); +} diff --git a/packages/cli/src/utils/gitCliCheck.test.ts b/packages/cli/src/utils/gitCliCheck.test.ts new file mode 100644 index 0000000..7ef16a4 --- /dev/null +++ b/packages/cli/src/utils/gitCliCheck.test.ts @@ -0,0 +1,138 @@ +import { exec } from 'child_process'; + +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { checkGitCli } from './gitCliCheck'; + +// Mock the child_process module +vi.mock('child_process', () => ({ + exec: vi.fn(), +})); + +// Mock the util module +vi.mock('util', () => ({ + promisify: vi.fn((fn) => { + return (cmd: string) => { + return new Promise((resolve, reject) => { + fn(cmd, (error: Error | null, result: { stdout: string }) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + }); + }; + }), +})); + +describe('gitCliCheck', () => { + const mockExec = exec as unknown as vi.Mock; + + beforeEach(() => { + mockExec.mockReset(); + }); + + it('should return all true when git and gh are available and authenticated', async () => { + // Mock successful responses + mockExec.mockImplementation( + ( + cmd: string, + callback: (error: Error | null, result: { stdout: string }) => void, + ) => { + if (cmd === 'git --version') { + callback(null, { stdout: 'git version 2.30.1' }); + } else if (cmd === 'gh --version') { + callback(null, { stdout: 'gh version 2.0.0' }); + } else if (cmd === 'gh auth status') { + callback(null, { stdout: 'Logged in to github.com as username' }); + } + }, + ); + + const result = await checkGitCli(); + + expect(result.gitAvailable).toBe(true); + expect(result.ghAvailable).toBe(true); + expect(result.ghAuthenticated).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should detect when git is not available', async () => { + mockExec.mockImplementation( + ( + cmd: string, + callback: (error: Error | null, result: { stdout: string }) => void, + ) => { + if (cmd === 'git --version') { + callback(new Error('Command not found'), { stdout: '' }); + } else if (cmd === 'gh --version') { + callback(null, { stdout: 'gh version 2.0.0' }); + } else if (cmd === 'gh auth status') { + callback(null, { stdout: 'Logged in to github.com as username' }); + } + }, + ); + + const result = await checkGitCli(); + + expect(result.gitAvailable).toBe(false); + expect(result.ghAvailable).toBe(true); + expect(result.ghAuthenticated).toBe(true); + expect(result.errors).toContain( + 'Git CLI is not available. Please install git.', + ); + }); + + it('should detect when gh is not available', async () => { + mockExec.mockImplementation( + ( + cmd: string, + callback: (error: Error | null, result: { stdout: string }) => void, + ) => { + if (cmd === 'git --version') { + callback(null, { stdout: 'git version 2.30.1' }); + } else if (cmd === 'gh --version') { + callback(new Error('Command not found'), { stdout: '' }); + } + }, + ); + + const result = await checkGitCli(); + + expect(result.gitAvailable).toBe(true); + expect(result.ghAvailable).toBe(false); + expect(result.ghAuthenticated).toBe(false); + expect(result.errors).toContain( + 'GitHub CLI is not available. Please install gh CLI.', + ); + }); + + it('should detect when gh is not authenticated', async () => { + mockExec.mockImplementation( + ( + cmd: string, + callback: (error: Error | null, result: { stdout: string }) => void, + ) => { + if (cmd === 'git --version') { + callback(null, { stdout: 'git version 2.30.1' }); + } else if (cmd === 'gh --version') { + callback(null, { stdout: 'gh version 2.0.0' }); + } else if (cmd === 'gh auth status') { + callback(new Error('You are not logged into any GitHub hosts'), { + stdout: '', + }); + } + }, + ); + + const result = await checkGitCli(); + + expect(result.gitAvailable).toBe(true); + expect(result.ghAvailable).toBe(true); + expect(result.ghAuthenticated).toBe(false); + expect(result.errors).toContain( + 'GitHub CLI is not authenticated. Please run "gh auth login".', + ); + }); +}); diff --git a/packages/cli/src/utils/gitCliCheck.ts b/packages/cli/src/utils/gitCliCheck.ts new file mode 100644 index 0000000..530b732 --- /dev/null +++ b/packages/cli/src/utils/gitCliCheck.ts @@ -0,0 +1,92 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; + +import { Logger } from 'mycoder-agent'; + +const execAsync = promisify(exec); + +/** + * Result of CLI tool checks + */ +export interface GitCliCheckResult { + gitAvailable: boolean; + ghAvailable: boolean; + ghAuthenticated: boolean; + errors: string[]; +} + +/** + * Checks if git command is available + */ +async function checkGitAvailable(): Promise { + try { + await execAsync('git --version'); + return true; + } catch { + return false; + } +} + +/** + * Checks if gh command is available + */ +async function checkGhAvailable(): Promise { + try { + await execAsync('gh --version'); + return true; + } catch { + return false; + } +} + +/** + * Checks if gh is authenticated + */ +async function checkGhAuthenticated(): Promise { + try { + const { stdout } = await execAsync('gh auth status'); + return stdout.includes('Logged in to'); + } catch { + return false; + } +} + +/** + * Checks if git and gh CLI tools are available and if gh is authenticated + * @param logger Optional logger for debug output + * @returns Object with check results + */ +export async function checkGitCli(logger?: Logger): Promise { + const result: GitCliCheckResult = { + gitAvailable: false, + ghAvailable: false, + ghAuthenticated: false, + errors: [], + }; + + logger?.debug('Checking for git CLI availability...'); + result.gitAvailable = await checkGitAvailable(); + + logger?.debug('Checking for gh CLI availability...'); + result.ghAvailable = await checkGhAvailable(); + + if (result.ghAvailable) { + logger?.debug('Checking for gh CLI authentication...'); + result.ghAuthenticated = await checkGhAuthenticated(); + } + + // Collect any errors + if (!result.gitAvailable) { + result.errors.push('Git CLI is not available. Please install git.'); + } + + if (!result.ghAvailable) { + result.errors.push('GitHub CLI is not available. Please install gh CLI.'); + } else if (!result.ghAuthenticated) { + result.errors.push( + 'GitHub CLI is not authenticated. Please run "gh auth login".', + ); + } + + return result; +} diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts deleted file mode 100644 index f6644cc..0000000 --- a/packages/cli/tests/cli.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { execSync } from 'child_process'; - -import { expect, test, describe } from 'vitest'; - -import { version } from '../package.json'; - -describe('CLI', () => { - test('version command outputs correct version', () => { - const output = execSync('node ./bin/cli.js --version').toString(); - expect(output).toContain(version); - expect(output).not.toContain('AI-powered coding assistant'); - }); - - test('--help command outputs help', () => { - const output = execSync('node ./bin/cli.js --help').toString(); - expect(output).toContain('Commands:'); - expect(output).toContain('Positionals:'); - expect(output).toContain('Options:'); - }); -}); diff --git a/packages/cli/tests/commands/config.test.ts b/packages/cli/tests/commands/config.test.ts deleted file mode 100644 index 7f0839a..0000000 --- a/packages/cli/tests/commands/config.test.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { Logger } from 'mycoder-agent'; -import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; - -import { command } from '../../src/commands/config.js'; -import { - getConfig, - getDefaultConfig, - updateConfig, - getConfigAtLevel, - clearConfigAtLevel, -} from '../../src/settings/config.js'; - -// Mock dependencies -vi.mock('../../src/settings/config.js', () => ({ - getConfig: vi.fn(), - getDefaultConfig: vi.fn(), - updateConfig: vi.fn(), - getConfigAtLevel: vi.fn(), - clearConfigAtLevel: vi.fn(), - ConfigLevel: { - DEFAULT: 'default', - GLOBAL: 'global', - PROJECT: 'project', - CLI: 'cli', - }, -})); - -vi.mock('mycoder-agent', () => ({ - Logger: vi.fn().mockImplementation(() => ({ - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - })), - LogLevel: { - debug: 0, - verbose: 1, - info: 2, - warn: 3, - error: 4, - }, -})); - -vi.mock('../../src/utils/nameToLogIndex.js', () => ({ - nameToLogIndex: vi.fn().mockReturnValue(2), // info level -})); - -// Mock readline/promises -vi.mock('readline/promises', () => ({ - createInterface: vi.fn().mockImplementation(() => ({ - question: vi.fn().mockResolvedValue('y'), - close: vi.fn(), - })), -})); - -describe('Config Command', () => { - let mockLogger: { - info: ReturnType; - error: ReturnType; - warn: ReturnType; - }; - - beforeEach(() => { - mockLogger = { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - }; - vi.mocked(Logger).mockImplementation(() => mockLogger as unknown as Logger); - vi.mocked(getConfig).mockReturnValue({ githubMode: false }); - vi.mocked(getDefaultConfig).mockReturnValue({ - githubMode: false, - customPrompt: '', - }); - vi.mocked(updateConfig).mockImplementation((config) => ({ - githubMode: false, - ...config, - })); - vi.mocked(getConfigAtLevel).mockReturnValue({}); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('should list all configuration values', async () => { - await command.handler!({ - _: ['config', 'list'], - logLevel: 'info', - interactive: false, - command: 'list', - global: false, - g: false, - } as any); - - expect(getConfig).toHaveBeenCalled(); - expect(mockLogger.info).toHaveBeenCalledWith('Current configuration:'); - expect(mockLogger.info).toHaveBeenCalledWith( - expect.stringContaining('githubMode'), - ); - }); - - it('should filter out invalid config keys in list command', async () => { - // Mock getConfig to return config with invalid keys - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - invalidKey: 'some value', - } as any); - - // Mock getDefaultConfig to return only valid keys - vi.mocked(getDefaultConfig).mockReturnValue({ - githubMode: false, - }); - - await command.handler!({ - _: ['config', 'list'], - logLevel: 'info', - interactive: false, - command: 'list', - global: false, - g: false, - } as any); - - expect(getConfig).toHaveBeenCalled(); - expect(getDefaultConfig).toHaveBeenCalled(); - - // Should show the valid key - expect(mockLogger.info).toHaveBeenCalledWith( - expect.stringContaining('githubMode'), - ); - - // Should not show the invalid key - const infoCallArgs = mockLogger.info.mock.calls.flat(); - expect(infoCallArgs.join()).not.toContain('invalidKey'); - }); - - it('should get a configuration value', async () => { - await command.handler!({ - _: ['config', 'get', 'githubMode'], - logLevel: 'info', - interactive: false, - command: 'get', - key: 'githubMode', - global: false, - g: false, - } as any); - - expect(getConfig).toHaveBeenCalled(); - expect(mockLogger.info).toHaveBeenCalledWith( - expect.stringContaining('githubMode'), - ); - }); - - it('should show error when getting non-existent key', async () => { - await command.handler!({ - _: ['config', 'get', 'nonExistentKey'], - logLevel: 'info', - interactive: false, - command: 'get', - key: 'nonExistentKey', - global: false, - g: false, - } as any); - - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('not found'), - ); - }); - - it('should set a configuration value', async () => { - await command.handler!({ - _: ['config', 'set', 'githubMode', 'true'], - logLevel: 'info', - interactive: false, - command: 'set', - key: 'githubMode', - value: 'true', - global: false, - g: false, - } as any); - - expect(updateConfig).toHaveBeenCalledWith({ githubMode: true }, 'project'); - expect(mockLogger.info).toHaveBeenCalledWith( - expect.stringContaining('Updated'), - ); - }); - - it('should handle missing key for set command', async () => { - await command.handler!({ - _: ['config', 'set'], - logLevel: 'info', - interactive: false, - command: 'set', - key: undefined, - global: false, - g: false, - } as any); - - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('Key is required'), - ); - }); - - it('should handle missing value for set command', async () => { - await command.handler!({ - _: ['config', 'set', 'githubMode'], - logLevel: 'info', - interactive: false, - command: 'set', - key: 'githubMode', - value: undefined, - global: false, - g: false, - } as any); - - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('Value is required'), - ); - }); - - it('should warn when setting non-standard key', async () => { - // Mock getDefaultConfig to return config without the key - vi.mocked(getDefaultConfig).mockReturnValue({ - customPrompt: '', - }); - - await command.handler!({ - _: ['config', 'set', 'nonStandardKey', 'value'], - logLevel: 'info', - interactive: false, - command: 'set', - key: 'nonStandardKey', - value: 'value', - global: false, - g: false, - } as any); - - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.stringContaining('not a standard configuration key'), - ); - // Should still update the config - expect(updateConfig).toHaveBeenCalled(); - }); - - it('should clear a configuration value', async () => { - // Mock getConfig to include the key we want to clear - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - customPrompt: 'custom value', - }); - - // Mock getDefaultConfig to include the key we want to clear - vi.mocked(getDefaultConfig).mockReturnValue({ - githubMode: false, - customPrompt: '', - }); - - await command.handler!({ - _: ['config', 'clear', 'customPrompt'], - logLevel: 'info', - interactive: false, - command: 'clear', - key: 'customPrompt', - global: false, - g: false, - all: false, - } as any); - - // Verify success message - expect(mockLogger.info).toHaveBeenCalledWith( - expect.stringContaining('Cleared customPrompt'), - ); - }); - - it('should handle missing key for clear command', async () => { - await command.handler!({ - _: ['config', 'clear'], - logLevel: 'info', - interactive: false, - command: 'clear', - key: undefined, - global: false, - g: false, - all: false, - } as any); - - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('Key is required'), - ); - }); - - it('should clear all project configuration with --all flag', async () => { - await command.handler!({ - _: ['config', 'clear'], - logLevel: 'info', - interactive: false, - command: 'clear', - all: true, - global: false, - g: false, - } as any); - - expect(clearConfigAtLevel).toHaveBeenCalledWith('project'); - expect(mockLogger.info).toHaveBeenCalledWith( - expect.stringContaining( - 'project configuration settings have been cleared', - ), - ); - }); - - it('should clear all global configuration with --all --global flags', async () => { - await command.handler!({ - _: ['config', 'clear'], - logLevel: 'info', - interactive: false, - command: 'clear', - all: true, - global: true, - g: false, - } as any); - - expect(clearConfigAtLevel).toHaveBeenCalledWith('global'); - expect(mockLogger.info).toHaveBeenCalledWith( - expect.stringContaining( - 'global configuration settings have been cleared', - ), - ); - }); - - it('should handle non-existent key for clear command', async () => { - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - }); - - await command.handler!({ - _: ['config', 'clear', 'nonExistentKey'], - logLevel: 'info', - interactive: false, - command: 'clear', - key: 'nonExistentKey', - global: false, - g: false, - all: false, - } as any); - - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('not found'), - ); - }); - - it('should handle unknown command', async () => { - await command.handler!({ - _: ['config', 'unknown'], - logLevel: 'info', - interactive: false, - command: 'unknown' as any, - global: false, - g: false, - } as any); - - expect(mockLogger.error).toHaveBeenCalledWith( - expect.stringContaining('Unknown config command'), - ); - }); - - it('should list all configuration values with default indicators', async () => { - // Mock getConfig to return a mix of default and custom values - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, // default value - customPrompt: 'custom value', // custom value - }); - - // Mock getDefaultConfig to return the default values - vi.mocked(getDefaultConfig).mockReturnValue({ - githubMode: false, - customPrompt: '', - }); - - await command.handler!({ - _: ['config', 'list'], - logLevel: 'info', - interactive: false, - command: 'list', - global: false, - g: false, - } as any); - - expect(getConfig).toHaveBeenCalled(); - expect(getDefaultConfig).toHaveBeenCalled(); - expect(mockLogger.info).toHaveBeenCalledWith('Current configuration:'); - - // Check for default indicator - expect(mockLogger.info).toHaveBeenCalledWith( - expect.stringContaining('githubMode') && - expect.stringContaining('(default)'), - ); - - // Check for custom value - const infoCallArgs = mockLogger.info.mock.calls.flat(); - const customPromptCall = infoCallArgs.find( - (arg) => typeof arg === 'string' && arg.includes('customPrompt'), - ); - expect(customPromptCall).toBeDefined(); - expect(customPromptCall).not.toContain('(default)'); - }); - - it('should use global config when --global flag is provided', async () => { - await command.handler!({ - _: ['config', 'set', 'githubMode', 'true'], - logLevel: 'info', - interactive: false, - command: 'set', - key: 'githubMode', - value: 'true', - global: true, - g: false, - } as any); - - expect(updateConfig).toHaveBeenCalledWith({ githubMode: true }, 'global'); - }); - - it('should use global config when -g flag is provided', async () => { - await command.handler!({ - _: ['config', 'set', 'githubMode', 'true'], - logLevel: 'info', - interactive: false, - command: 'set', - key: 'githubMode', - value: 'true', - global: false, - g: true, - } as any); - - expect(updateConfig).toHaveBeenCalledWith({ githubMode: true }, 'global'); - }); -}); diff --git a/packages/cli/tests/settings/config-defaults.test.ts b/packages/cli/tests/settings/config-defaults.test.ts deleted file mode 100644 index d70bc72..0000000 --- a/packages/cli/tests/settings/config-defaults.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { toolAgent } from 'mycoder-agent'; -import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; - -import { getConfig } from '../../src/settings/config.js'; - -// Mock dependencies -vi.mock('../../src/settings/config.js', () => ({ - getConfig: vi.fn(), - updateConfig: vi.fn(), -})); - -vi.mock('mycoder-agent', () => ({ - Logger: vi.fn().mockImplementation(() => ({ - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - log: vi.fn(), - debug: vi.fn(), - })), - toolAgent: vi.fn().mockResolvedValue({ result: 'Success' }), - getTools: vi.fn().mockReturnValue([]), - getAnthropicApiKeyError: vi.fn(), - userPrompt: vi.fn(), - LogLevel: { - debug: 0, - verbose: 1, - info: 2, - warn: 3, - error: 4, - }, - subAgentTool: { logPrefix: '' }, - errorToString: vi.fn(), - getModel: vi.fn(), - DEFAULT_CONFIG: {}, - TokenTracker: vi.fn().mockImplementation(() => ({ - logLevel: 2, - toString: () => 'token usage', - })), -})); - -describe('Config Defaults for CLI Options', () => { - beforeEach(() => { - // Mock process.env - process.env.ANTHROPIC_API_KEY = 'test-key'; - - // Reset mocks before each test - vi.resetAllMocks(); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('should use config values for headless, userSession, and pageFilter when not provided in args', async () => { - // Setup mock config with default values - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - headless: true, - userSession: false, - pageFilter: 'none', - provider: 'anthropic', - model: 'claude-3-7-sonnet-20250219', - maxTokens: 0, - temperature: 0, - customPrompt: '', - profile: false, - tokenCache: false, - ANTHROPIC_API_KEY: '', - }); - - // Create minimal args (no headless, userSession, or pageFilter specified) - const args = { - headless: undefined, - userSession: undefined, - pageFilter: undefined, - }; - - // Get config from getConfig - const config = getConfig(); - - // Simulate how $default.ts uses these values - const options = { - headless: args.headless ?? config.headless, - userSession: args.userSession ?? config.userSession, - pageFilter: args.pageFilter ?? config.pageFilter, - }; - - // Verify the correct values are used (from config) - expect(options).toEqual({ - headless: true, // Default from config - userSession: false, // Default from config - pageFilter: 'none', // Default from config - }); - }); - - it('should use command line args for headless, userSession, and pageFilter when provided', async () => { - // Setup mock config with default values - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - headless: true, // Default is true - userSession: false, // Default is false - pageFilter: 'none', // Default is none - provider: 'anthropic', - model: 'claude-3-7-sonnet-20250219', - }); - - // Create args with explicit values (overriding defaults) - const args = { - headless: false, // Override config default - userSession: true, // Override config default - pageFilter: 'readability', // Override config default - }; - - // Get config from getConfig - const config = getConfig(); - - // Simulate how $default.ts uses these values - const options = { - headless: args.headless ?? config.headless, - userSession: args.userSession ?? config.userSession, - pageFilter: args.pageFilter ?? config.pageFilter, - }; - - // Verify the correct values are used (from command line args) - expect(options).toEqual({ - headless: false, // Overridden by command line - userSession: true, // Overridden by command line - pageFilter: 'readability', // Overridden by command line - }); - }); - - it('should test the actual toolAgent call with config defaults', async () => { - // Setup mock config with default values - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - headless: true, - userSession: false, - pageFilter: 'none', - provider: 'anthropic', - model: 'claude-3-7-sonnet-20250219', - }); - - // Create minimal args (no headless, userSession, or pageFilter specified) - const args = { - headless: undefined, - userSession: undefined, - pageFilter: undefined, - }; - - // Get config from getConfig - const config = getConfig(); - - // Call toolAgent with the config values - await toolAgent( - 'test prompt', - [], - {}, - { - headless: args.headless ?? config.headless, - userSession: args.userSession ?? config.userSession, - pageFilter: args.pageFilter ?? config.pageFilter, - workingDirectory: '.', - githubMode: config.githubMode, - }, - ); - - // Verify toolAgent was called with the correct config values from defaults - expect(toolAgent).toHaveBeenCalledWith( - 'test prompt', - expect.any(Array), - expect.any(Object), - expect.objectContaining({ - headless: true, // Default from config - userSession: false, // Default from config - pageFilter: 'none', // Default from config - }), - ); - }); - - it('should test the actual toolAgent call with command line args', async () => { - // Setup mock config with default values - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - headless: true, // Default is true - userSession: false, // Default is false - pageFilter: 'none', // Default is none - provider: 'anthropic', - model: 'claude-3-7-sonnet-20250219', - }); - - // Create args with explicit values (overriding defaults) - const args = { - headless: false, // Override config default - userSession: true, // Override config default - pageFilter: 'readability', // Override config default - }; - - // Get config from getConfig - const config = getConfig(); - - // Call toolAgent with the command line args - await toolAgent( - 'test prompt', - [], - {}, - { - headless: args.headless ?? config.headless, - userSession: args.userSession ?? config.userSession, - pageFilter: args.pageFilter ?? config.pageFilter, - workingDirectory: '.', - githubMode: config.githubMode, - }, - ); - - // Verify toolAgent was called with the command line args (overriding defaults) - expect(toolAgent).toHaveBeenCalledWith( - 'test prompt', - expect.any(Array), - expect.any(Object), - expect.objectContaining({ - headless: false, // Overridden by command line - userSession: true, // Overridden by command line - pageFilter: 'readability', // Overridden by command line - }), - ); - }); -}); diff --git a/packages/cli/tests/settings/config.test.ts b/packages/cli/tests/settings/config.test.ts deleted file mode 100644 index 450db5b..0000000 --- a/packages/cli/tests/settings/config.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; - -import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; - -import { updateConfig } from '../../src/settings/config.js'; -import { getSettingsDir } from '../../src/settings/settings.js'; - -// Mock getProjectConfigFile -vi.mock( - '../../src/settings/config.js', - async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getProjectConfigFile: vi - .fn() - .mockReturnValue('/mock/project/dir/.mycoder/config.json'), - }; - }, - { partial: true }, -); - -// Mock the settings directory -vi.mock('../../src/settings/settings.js', () => { - return { - getSettingsDir: vi.fn().mockReturnValue('/mock/settings/dir'), - getProjectSettingsDir: vi - .fn() - .mockReturnValue('/mock/project/dir/.mycoder'), - isProjectSettingsDirWritable: vi.fn().mockReturnValue(true), - }; -}); - -// Mock fs module -vi.mock('fs', () => ({ - existsSync: vi.fn(), - readFileSync: vi.fn(), - writeFileSync: vi.fn(), -})); - -describe('Config', () => { - const mockSettingsDir = '/mock/settings/dir'; - const mockConfigFile = path.join(mockSettingsDir, 'config.json'); - - beforeEach(() => { - vi.mocked(getSettingsDir).mockReturnValue(mockSettingsDir); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - beforeEach(() => { - // Reset all mocks before each test - vi.resetAllMocks(); - - // Set test environment - process.env.VITEST = 'true'; - }); - - describe('updateConfig', () => { - it('should update config and write to file', () => { - const currentConfig = { githubMode: false }; - const newConfig = { githubMode: true }; - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(currentConfig)); - - // Force using GLOBAL level to avoid project directory issues - const result = updateConfig(newConfig, 'global'); - - expect(result).toEqual({ githubMode: true }); - expect(fs.writeFileSync).toHaveBeenCalledWith( - mockConfigFile, - JSON.stringify({ githubMode: true }, null, 2), - ); - }); - - it('should merge partial config with existing config', () => { - const currentConfig = { githubMode: false, existingSetting: 'value' }; - const partialConfig = { githubMode: true }; - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(currentConfig)); - - // In test mode, updateConfig returns just the config that was passed in - // This is a limitation of our test approach - updateConfig(partialConfig, 'global'); - - // Just verify the write was called with the right data - expect(fs.writeFileSync).toHaveBeenCalledWith( - mockConfigFile, - JSON.stringify({ githubMode: true, existingSetting: 'value' }, null, 2), - ); - }); - }); -}); diff --git a/packages/cli/tests/settings/configDefaults.test.ts b/packages/cli/tests/settings/configDefaults.test.ts deleted file mode 100644 index c85a504..0000000 --- a/packages/cli/tests/settings/configDefaults.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { toolAgent } from 'mycoder-agent'; -import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; - -import { getConfig } from '../../src/settings/config.js'; - -// Mock dependencies -vi.mock('../../src/settings/config.js', () => ({ - getConfig: vi.fn(), - updateConfig: vi.fn(), -})); - -vi.mock('mycoder-agent', () => ({ - Logger: vi.fn().mockImplementation(() => ({ - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - log: vi.fn(), - debug: vi.fn(), - })), - toolAgent: vi.fn().mockResolvedValue({ result: 'Success' }), - getTools: vi.fn().mockReturnValue([]), - getAnthropicApiKeyError: vi.fn(), - userPrompt: vi.fn(), - LogLevel: { - debug: 0, - verbose: 1, - info: 2, - warn: 3, - error: 4, - }, - subAgentTool: { logPrefix: '' }, - errorToString: vi.fn(), - getModel: vi.fn(), - DEFAULT_CONFIG: {}, - TokenTracker: vi.fn().mockImplementation(() => ({ - logLevel: 2, - toString: () => 'token usage', - })), -})); - -describe('Config Defaults for CLI Options', () => { - beforeEach(() => { - // Mock process.env - process.env.ANTHROPIC_API_KEY = 'test-key'; - - // Reset mocks before each test - vi.resetAllMocks(); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('should use config values for headless, userSession, and pageFilter when not provided in args', async () => { - // Setup mock config with default values - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - headless: true, - userSession: false, - pageFilter: 'none', - provider: 'anthropic', - model: 'claude-3-7-sonnet-20250219', - }); - - // Create minimal args (no headless, userSession, or pageFilter specified) - const args = { - headless: undefined, - userSession: undefined, - pageFilter: undefined, - }; - - // Get config from getConfig - const config = getConfig(); - - // Simulate how $default.ts uses these values - const options = { - headless: args.headless ?? config.headless, - userSession: args.userSession ?? config.userSession, - pageFilter: args.pageFilter ?? config.pageFilter, - }; - - // Verify the correct values are used (from config) - expect(options).toEqual({ - headless: true, // Default from config - userSession: false, // Default from config - pageFilter: 'none', // Default from config - }); - }); - - it('should use command line args for headless, userSession, and pageFilter when provided', async () => { - // Setup mock config with default values - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - headless: true, // Default is true - userSession: false, // Default is false - pageFilter: 'none', // Default is none - provider: 'anthropic', - model: 'claude-3-7-sonnet-20250219', - }); - - // Create args with explicit values (overriding defaults) - const args = { - headless: false, // Override config default - userSession: true, // Override config default - pageFilter: 'readability', // Override config default - }; - - // Get config from getConfig - const config = getConfig(); - - // Simulate how $default.ts uses these values - const options = { - headless: args.headless ?? config.headless, - userSession: args.userSession ?? config.userSession, - pageFilter: args.pageFilter ?? config.pageFilter, - }; - - // Verify the correct values are used (from command line args) - expect(options).toEqual({ - headless: false, // Overridden by command line - userSession: true, // Overridden by command line - pageFilter: 'readability', // Overridden by command line - }); - }); - - it('should test the actual toolAgent call with config defaults', async () => { - // Setup mock config with default values - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - headless: true, - userSession: false, - pageFilter: 'none', - provider: 'anthropic', - model: 'claude-3-7-sonnet-20250219', - }); - - // Create minimal args (no headless, userSession, or pageFilter specified) - const args = { - headless: undefined, - userSession: undefined, - pageFilter: undefined, - }; - - // Get config from getConfig - const config = getConfig(); - - // Call toolAgent with the config values - await toolAgent( - 'test prompt', - [], - {}, - { - headless: args.headless ?? config.headless, - userSession: args.userSession ?? config.userSession, - pageFilter: args.pageFilter ?? config.pageFilter, - workingDirectory: '.', - githubMode: config.githubMode, - }, - ); - - // Verify toolAgent was called with the correct config values from defaults - expect(toolAgent).toHaveBeenCalledWith( - 'test prompt', - expect.any(Array), - expect.any(Object), - expect.objectContaining({ - headless: true, // Default from config - userSession: false, // Default from config - pageFilter: 'none', // Default from config - }), - ); - }); - - it('should test the actual toolAgent call with command line args', async () => { - // Setup mock config with default values - vi.mocked(getConfig).mockReturnValue({ - githubMode: false, - headless: true, // Default is true - userSession: false, // Default is false - pageFilter: 'none', // Default is none - provider: 'anthropic', - model: 'claude-3-7-sonnet-20250219', - }); - - // Create args with explicit values (overriding defaults) - const args = { - headless: false, // Override config default - userSession: true, // Override config default - pageFilter: 'readability', // Override config default - }; - - // Get config from getConfig - const config = getConfig(); - - // Call toolAgent with the command line args - await toolAgent( - 'test prompt', - [], - {}, - { - headless: args.headless ?? config.headless, - userSession: args.userSession ?? config.userSession, - pageFilter: args.pageFilter ?? config.pageFilter, - workingDirectory: '.', - githubMode: config.githubMode, - }, - ); - - // Verify toolAgent was called with the command line args (overriding defaults) - expect(toolAgent).toHaveBeenCalledWith( - 'test prompt', - expect.any(Array), - expect.any(Object), - expect.objectContaining({ - headless: false, // Overridden by command line - userSession: true, // Overridden by command line - pageFilter: 'readability', // Overridden by command line - }), - ); - }); -}); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 204fe19..5954c75 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -44,5 +44,6 @@ "allowJs": false, "checkJs": false }, - "include": ["src/**/*"] + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ba9194..38054a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,9 +12,9 @@ importers: specifier: ^6.0.1 version: 6.0.1 devDependencies: - '@changesets/cli': - specifier: ^2.28.1 - version: 2.28.1 + '@anolilab/semantic-release-pnpm': + specifier: ^1.1.10 + version: 1.1.10(@types/node@18.19.80)(yaml@2.7.0) '@commitlint/cli': specifier: ^19.7.1 version: 19.8.0(@types/node@18.19.80)(typescript@5.8.2) @@ -39,6 +39,12 @@ importers: '@typescript-eslint/parser': specifier: ^8.23.0 version: 8.26.1(eslint@9.22.0(jiti@2.4.2))(typescript@5.8.2) + commitizen: + specifier: ^4.3.1 + version: 4.3.1(@types/node@18.19.80)(typescript@5.8.2) + cz-conventional-changelog: + specifier: ^3.3.0 + version: 3.3.0(@types/node@18.19.80)(typescript@5.8.2) eslint: specifier: ^9.0.0 version: 9.22.0(jiti@2.4.2) @@ -102,6 +108,9 @@ importers: jsdom: specifier: ^26.0.0 version: 26.0.0 + openai: + specifier: ^4.87.3 + version: 4.87.3(encoding@0.1.13)(ws@8.18.1)(zod@3.24.2) playwright: specifier: ^1.50.1 version: 1.51.0 @@ -142,6 +151,9 @@ importers: chalk: specifier: ^5 version: 5.4.1 + cosmiconfig: + specifier: ^9.0.0 + version: 9.0.0(typescript@5.8.2) deepmerge: specifier: ^4.3.1 version: 4.3.1 @@ -197,6 +209,17 @@ importers: packages: + '@anolilab/rc@1.1.6': + resolution: {integrity: sha512-jqalzF9dYCN8EYVgqCeZG9IEMFIgi3A8xv8bIsXIxrBW9hapJIzpa0ZT0JS1XQUVxOh7mV51dLFHmjevCGu6xg==} + engines: {node: '>=18.* <=23.*'} + + '@anolilab/semantic-release-pnpm@1.1.10': + resolution: {integrity: sha512-gvZx4eKjDyAZF52pjiQc6ic8TKJ1IyT7UYy5dyIrx8rQhOC2mTL6EwNh9ayxS7TmPED5nK5qvz77u+1GH4f9fA==} + engines: {node: '>=18.* <=23.*'} + + '@antfu/install-pkg@1.0.0': + resolution: {integrity: sha512-xvX6P/lo1B3ej0OsaErAjqgFYzYVcJpamjLAFLYh9vRJngBrMoUG7aVnrGTeqM7yxbyTD5p3F2+0/QUEh8Vzhw==} + '@anthropic-ai/sdk@0.37.0': resolution: {integrity: sha512-tHjX2YbkUBwEgg0JZU3EFSSAQPoK4qQR/NFYa8Vtzd5UAyXzZksCw2In69Rml4R/TyHPBfRYaLK35XiOe33pjw==} @@ -224,61 +247,6 @@ packages: '@bundled-es-modules/tough-cookie@0.1.6': resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} - '@changesets/apply-release-plan@7.0.10': - resolution: {integrity: sha512-wNyeIJ3yDsVspYvHnEz1xQDq18D9ifed3lI+wxRQRK4pArUcuHgCTrHv0QRnnwjhVCQACxZ+CBih3wgOct6UXw==} - - '@changesets/assemble-release-plan@6.0.6': - resolution: {integrity: sha512-Frkj8hWJ1FRZiY3kzVCKzS0N5mMwWKwmv9vpam7vt8rZjLL1JMthdh6pSDVSPumHPshTTkKZ0VtNbE0cJHZZUg==} - - '@changesets/changelog-git@0.2.1': - resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - - '@changesets/cli@2.28.1': - resolution: {integrity: sha512-PiIyGRmSc6JddQJe/W1hRPjiN4VrMvb2VfQ6Uydy2punBioQrsxppyG5WafinKcW1mT0jOe/wU4k9Zy5ff21AA==} - hasBin: true - - '@changesets/config@3.1.1': - resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} - - '@changesets/errors@0.2.0': - resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} - - '@changesets/get-dependents-graph@2.1.3': - resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - - '@changesets/get-release-plan@4.0.8': - resolution: {integrity: sha512-MM4mq2+DQU1ZT7nqxnpveDMTkMBLnwNX44cX7NSxlXmr7f8hO6/S2MXNiXG54uf/0nYnefv0cfy4Czf/ZL/EKQ==} - - '@changesets/get-version-range-type@0.4.0': - resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} - - '@changesets/git@3.0.2': - resolution: {integrity: sha512-r1/Kju9Y8OxRRdvna+nxpQIsMsRQn9dhhAZt94FLDeu0Hij2hnOozW8iqnHBgvu+KdnJppCveQwK4odwfw/aWQ==} - - '@changesets/logger@0.1.1': - resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} - - '@changesets/parse@0.4.1': - resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} - - '@changesets/pre@2.0.2': - resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} - - '@changesets/read@0.6.3': - resolution: {integrity: sha512-9H4p/OuJ3jXEUTjaVGdQEhBdqoT2cO5Ts95JTFsQyawmKzpL8FnIeJSyhTDPW1MBRDnwZlHFEM9SpPwJDY5wIg==} - - '@changesets/should-skip-package@0.1.2': - resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} - - '@changesets/types@4.1.0': - resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} - - '@changesets/types@6.1.0': - resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} - - '@changesets/write@0.4.0': - resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -626,12 +594,6 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@manypkg/find-root@1.1.0': - resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} - - '@manypkg/get-packages@1.1.3': - resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@mozilla/readability@0.5.0': resolution: {integrity: sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==} engines: {node: '>=14.0.0'} @@ -1153,9 +1115,6 @@ packages: '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@18.19.80': resolution: {integrity: sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==} @@ -1236,6 +1195,26 @@ packages: resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@visulima/fs@3.1.2': + resolution: {integrity: sha512-LZ9GLLxVfuaFzOGb2zp4GOqyT7TcLmnEShayrb1S2n0WuA3Pfig8fx42xaHyPTZ1p4pI3ncDNTmbyg1BIYM9rw==} + engines: {node: '>=18.0.0 <=23.x'} + os: [darwin, linux, win32] + peerDependencies: + yaml: ^2.7.0 + peerDependenciesMeta: + yaml: + optional: true + + '@visulima/package@3.5.3': + resolution: {integrity: sha512-FeUgWy0ZkrZ9tCfKRR6yTg11IsE9fwXRnzjovbMHK4SPi01BvyMIWYKUqHG6t3RCO87Qcl6PvIup+zP8+wdM8w==} + engines: {node: '>=18.0.0 <=23.x'} + os: [darwin, linux, win32] + + '@visulima/path@1.3.5': + resolution: {integrity: sha512-9kK3QgVxuR/XkafDCANEuJjQsoKIXZxh4ejmlCm7cWgnaH9uFTzSwU/8ifMEg4cjYDcBYeRAGDq1zBrC03ZY1w==} + engines: {node: '>=18.0.0 <=23.x'} + os: [darwin, linux, win32] + '@vitest/browser@3.0.8': resolution: {integrity: sha512-ARAGav2gJE/t+qF44fOwJlK0dK8ZJEYjZ725ewHzN6liBAJSCt9elqv/74iwjl5RJzel00k/wufJB7EEu+MJEw==} peerDependencies: @@ -1325,10 +1304,6 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -1364,9 +1339,6 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1418,6 +1390,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1425,12 +1401,14 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} - better-path-resolve@1.0.0: - resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} - engines: {node: '>=4'} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} @@ -1448,10 +1426,17 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cachedir@2.3.0: + resolution: {integrity: sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==} + engines: {node: '>=6'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1495,10 +1480,6 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -1510,6 +1491,10 @@ packages: resolution: {integrity: sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==} engines: {node: '>=14.16'} + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -1519,6 +1504,10 @@ packages: engines: {node: '>=8.0.0', npm: '>=5.0.0'} hasBin: true + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} @@ -1527,6 +1516,10 @@ packages: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1538,6 +1531,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1562,6 +1559,11 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} + commitizen@4.3.1: + resolution: {integrity: sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw==} + engines: {node: '>= 12'} + hasBin: true + compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} @@ -1588,6 +1590,9 @@ packages: engines: {node: '>=18'} hasBin: true + conventional-commit-types@3.0.0: + resolution: {integrity: sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==} + conventional-commits-filter@5.0.0: resolution: {integrity: sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==} engines: {node: '>=18'} @@ -1646,6 +1651,10 @@ packages: resolution: {integrity: sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==} engines: {node: '>=18'} + cz-conventional-changelog@3.3.0: + resolution: {integrity: sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==} + engines: {node: '>= 10'} + dargs@8.1.0: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} @@ -1686,6 +1695,9 @@ packages: decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} + dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1701,6 +1713,9 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -1721,6 +1736,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-file@1.0.0: + resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==} + engines: {node: '>=0.10.0'} + detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} @@ -1773,10 +1792,6 @@ packages: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} - enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1956,11 +1971,6 @@ packages: resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -1999,13 +2009,14 @@ packages: resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} engines: {node: ^18.19.0 || >=20.5.0} + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + expect-type@1.2.0: resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} engines: {node: '>=12.0.0'} - extendable-error@0.1.7: - resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} - external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -2047,6 +2058,10 @@ packages: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} engines: {node: '>=4'} + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -2063,6 +2078,12 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-node-modules@2.1.3: + resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} @@ -2075,10 +2096,6 @@ packages: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2091,6 +2108,10 @@ packages: resolution: {integrity: sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==} engines: {node: '>=18'} + findup-sync@4.0.0: + resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==} + engines: {node: '>= 8'} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2131,13 +2152,9 @@ packages: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - - fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2238,6 +2255,14 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2313,6 +2338,10 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + hook-std@3.0.0: resolution: {integrity: sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2340,10 +2369,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - human-id@4.1.1: - resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} - hasBin: true - human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2372,6 +2397,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2424,6 +2452,14 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@5.0.0: + resolution: {integrity: sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==} + engines: {node: ^18.17.0 || >=20.5.0} + + inquirer@8.2.5: + resolution: {integrity: sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==} + engines: {node: '>=12.0.0'} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -2498,6 +2534,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -2560,10 +2600,6 @@ packages: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} - is-subdir@1.2.0: - resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} - engines: {node: '>=4'} - is-symbol@1.1.1: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} @@ -2576,10 +2612,17 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-utf8@0.2.1: + resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -2627,10 +2670,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -2666,9 +2705,6 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -2711,10 +2747,6 @@ packages: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2744,6 +2776,9 @@ packages: lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + lodash.map@4.6.0: + resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2768,10 +2803,18 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + longest@2.0.1: + resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} + engines: {node: '>=0.10.0'} + loupe@3.1.3: resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} @@ -2819,6 +2862,9 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + merge@2.1.1: + resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2859,6 +2905,9 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -2869,10 +2918,6 @@ packages: module-details-from-path@1.0.3: resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -2890,6 +2935,9 @@ packages: typescript: optional: true + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -2935,6 +2983,10 @@ packages: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} engines: {node: ^16.14.0 || >=18.0.0} + normalize-package-data@7.0.0: + resolution: {integrity: sha512-k6U0gKRIuNCTkwHGZqblCfLfBRh+w1vI6tBo+IeJwq2M8FUiOqhX7GH+GArQGScA7azd1WfyRCvxoXDO3hQDIA==} + engines: {node: ^18.17.0 || >=20.5.0} + normalize-url@8.0.1: resolution: {integrity: sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==} engines: {node: '>=14.16'} @@ -3071,17 +3123,30 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + openai@4.87.3: + resolution: {integrity: sha512-d2D54fzMuBYTxMW8wcNmhT1rYKcTfMJ8t+4KjH2KtvYenygITiGBgHoIrzHwnDQWW+C5oCA+ikIR2jgPCFqcKQ==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} - outdent@0.5.0: - resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -3097,10 +3162,6 @@ packages: resolution: {integrity: sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==} engines: {node: '>=12'} - p-filter@2.1.0: - resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} - engines: {node: '>=8'} - p-filter@4.1.0: resolution: {integrity: sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==} engines: {node: '>=18'} @@ -3133,10 +3194,6 @@ packages: resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} engines: {node: '>=6'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -3145,10 +3202,6 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-map@2.1.0: - resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} - engines: {node: '>=6'} - p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} @@ -3199,6 +3252,10 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + parse5-htmlparser2-tree-adapter@6.0.1: resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} @@ -3295,10 +3352,6 @@ packages: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - pkg-conf@2.1.0: resolution: {integrity: sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==} engines: {node: '>=4'} @@ -3349,11 +3402,6 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - prettier@3.5.3: resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} @@ -3411,13 +3459,13 @@ packages: resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} engines: {node: '>=18'} - read-yaml-file@1.1.0: - resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} - engines: {node: '>=6'} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3448,6 +3496,10 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3464,6 +3516,10 @@ packages: engines: {node: '>= 0.4'} hasBin: true + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} @@ -3497,9 +3553,16 @@ packages: rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -3649,9 +3712,6 @@ packages: spawn-error-forwarder@1.0.0: resolution: {integrity: sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==} - spawndamnit@3.0.1: - resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} - spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -3671,9 +3731,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} @@ -3736,6 +3793,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -3803,10 +3864,6 @@ packages: resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} engines: {node: '>=14.16'} - term-size@2.2.1: - resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} - engines: {node: '>=8'} - text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -3894,6 +3951,10 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-deepmerge@7.0.2: + resolution: {integrity: sha512-akcpDTPuez4xzULo5NwuoKwYRtjQJ9eoNfBACiBMaXwNAx7B1PKfe5wqUFJuW5uKzQ68YjDFwPaWHDG1KnFGsA==} + engines: {node: '>=14.13.1'} + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -3991,10 +4052,6 @@ packages: universal-user-agent@7.0.2: resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -4100,6 +4157,9 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} @@ -4142,6 +4202,10 @@ packages: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4258,6 +4322,36 @@ packages: snapshots: + '@anolilab/rc@1.1.6(yaml@2.7.0)': + dependencies: + '@visulima/fs': 3.1.2(yaml@2.7.0) + '@visulima/path': 1.3.5 + ini: 5.0.0 + ts-deepmerge: 7.0.2 + transitivePeerDependencies: + - yaml + + '@anolilab/semantic-release-pnpm@1.1.10(@types/node@18.19.80)(yaml@2.7.0)': + dependencies: + '@anolilab/rc': 1.1.6(yaml@2.7.0) + '@semantic-release/error': 4.0.0 + '@visulima/fs': 3.1.2(yaml@2.7.0) + '@visulima/package': 3.5.3(@types/node@18.19.80)(yaml@2.7.0) + '@visulima/path': 1.3.5 + execa: 9.5.2 + ini: 5.0.0 + normalize-url: 8.0.1 + registry-auth-token: 5.1.0 + semver: 7.7.1 + transitivePeerDependencies: + - '@types/node' + - yaml + + '@antfu/install-pkg@1.0.0': + dependencies: + package-manager-detector: 0.2.11 + tinyexec: 0.3.2 + '@anthropic-ai/sdk@0.37.0(encoding@0.1.13)': dependencies: '@types/node': 18.19.80 @@ -4303,148 +4397,6 @@ snapshots: '@types/tough-cookie': 4.0.5 tough-cookie: 4.1.4 - '@changesets/apply-release-plan@7.0.10': - dependencies: - '@changesets/config': 3.1.1 - '@changesets/get-version-range-type': 0.4.0 - '@changesets/git': 3.0.2 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - detect-indent: 6.1.0 - fs-extra: 7.0.1 - lodash.startcase: 4.4.0 - outdent: 0.5.0 - prettier: 2.8.8 - resolve-from: 5.0.0 - semver: 7.7.1 - - '@changesets/assemble-release-plan@6.0.6': - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - semver: 7.7.1 - - '@changesets/changelog-git@0.2.1': - dependencies: - '@changesets/types': 6.1.0 - - '@changesets/cli@2.28.1': - dependencies: - '@changesets/apply-release-plan': 7.0.10 - '@changesets/assemble-release-plan': 6.0.6 - '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.1 - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.8 - '@changesets/git': 3.0.2 - '@changesets/logger': 0.1.1 - '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.3 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@changesets/write': 0.4.0 - '@manypkg/get-packages': 1.1.3 - ansi-colors: 4.1.3 - ci-info: 3.9.0 - enquirer: 2.4.1 - external-editor: 3.1.0 - fs-extra: 7.0.1 - mri: 1.2.0 - p-limit: 2.3.0 - package-manager-detector: 0.2.11 - picocolors: 1.1.1 - resolve-from: 5.0.0 - semver: 7.7.1 - spawndamnit: 3.0.1 - term-size: 2.2.1 - - '@changesets/config@3.1.1': - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/logger': 0.1.1 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 - micromatch: 4.0.8 - - '@changesets/errors@0.2.0': - dependencies: - extendable-error: 0.1.7 - - '@changesets/get-dependents-graph@2.1.3': - dependencies: - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - picocolors: 1.1.1 - semver: 7.7.1 - - '@changesets/get-release-plan@4.0.8': - dependencies: - '@changesets/assemble-release-plan': 6.0.6 - '@changesets/config': 3.1.1 - '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.3 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - - '@changesets/get-version-range-type@0.4.0': {} - - '@changesets/git@3.0.2': - dependencies: - '@changesets/errors': 0.2.0 - '@manypkg/get-packages': 1.1.3 - is-subdir: 1.2.0 - micromatch: 4.0.8 - spawndamnit: 3.0.1 - - '@changesets/logger@0.1.1': - dependencies: - picocolors: 1.1.1 - - '@changesets/parse@0.4.1': - dependencies: - '@changesets/types': 6.1.0 - js-yaml: 3.14.1 - - '@changesets/pre@2.0.2': - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 - - '@changesets/read@0.6.3': - dependencies: - '@changesets/git': 3.0.2 - '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.1 - '@changesets/types': 6.1.0 - fs-extra: 7.0.1 - p-filter: 2.1.0 - picocolors: 1.1.1 - - '@changesets/should-skip-package@0.1.2': - dependencies: - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - - '@changesets/types@4.1.0': {} - - '@changesets/types@6.1.0': {} - - '@changesets/write@0.4.0': - dependencies: - '@changesets/types': 6.1.0 - fs-extra: 7.0.1 - human-id: 4.1.1 - prettier: 2.8.8 - '@colors/colors@1.5.0': optional: true @@ -4747,22 +4699,6 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} - '@manypkg/find-root@1.1.0': - dependencies: - '@babel/runtime': 7.26.10 - '@types/node': 12.20.55 - find-up: 4.1.0 - fs-extra: 8.1.0 - - '@manypkg/get-packages@1.1.3': - dependencies: - '@babel/runtime': 7.26.10 - '@changesets/types': 4.1.0 - '@manypkg/find-root': 1.1.0 - fs-extra: 8.1.0 - globby: 11.1.0 - read-yaml-file: 1.1.0 - '@mozilla/readability@0.5.0': {} '@mswjs/interceptors@0.37.6': @@ -5392,8 +5328,6 @@ snapshots: '@types/node': 18.19.80 form-data: 4.0.2 - '@types/node@12.20.55': {} - '@types/node@18.19.80': dependencies: undici-types: 5.26.5 @@ -5505,6 +5439,25 @@ snapshots: '@typescript-eslint/types': 8.26.1 eslint-visitor-keys: 4.2.0 + '@visulima/fs@3.1.2(yaml@2.7.0)': + dependencies: + '@visulima/path': 1.3.5 + optionalDependencies: + yaml: 2.7.0 + + '@visulima/package@3.5.3(@types/node@18.19.80)(yaml@2.7.0)': + dependencies: + '@antfu/install-pkg': 1.0.0 + '@inquirer/confirm': 5.1.7(@types/node@18.19.80) + '@visulima/fs': 3.1.2(yaml@2.7.0) + '@visulima/path': 1.3.5 + normalize-package-data: 7.0.0 + transitivePeerDependencies: + - '@types/node' + - yaml + + '@visulima/path@1.3.5': {} + '@vitest/browser@3.0.8(@testing-library/dom@10.4.0)(@types/node@18.19.80)(playwright@1.51.0)(typescript@5.8.2)(vite@6.2.1(@types/node@18.19.80)(jiti@2.4.2)(yaml@2.7.0))(vitest@3.0.8)': dependencies: '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.0) @@ -5616,8 +5569,6 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -5644,10 +5595,6 @@ snapshots: any-promise@1.3.0: {} - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - argparse@2.0.1: {} argv-formatter@1.0.0: {} @@ -5713,17 +5660,23 @@ snapshots: asynckit@0.4.0: {} + at-least-node@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 balanced-match@1.0.2: {} + base64-js@1.5.1: {} + before-after-hook@3.0.2: {} - better-path-resolve@1.0.0: + bl@4.1.0: dependencies: - is-windows: 1.0.2 + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 bottleneck@2.19.5: {} @@ -5742,8 +5695,15 @@ snapshots: buffer-from@1.1.2: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + cac@6.7.14: {} + cachedir@2.3.0: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5790,8 +5750,6 @@ snapshots: check-error@2.1.1: {} - ci-info@3.9.0: {} - cjs-module-lexer@1.4.3: {} clean-stack@2.2.0: {} @@ -5800,6 +5758,10 @@ snapshots: dependencies: escape-string-regexp: 5.0.0 + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -5813,6 +5775,8 @@ snapshots: parse5-htmlparser2-tree-adapter: 6.0.1 yargs: 16.2.0 + cli-spinners@2.9.2: {} + cli-table3@0.6.5: dependencies: string-width: 4.2.3 @@ -5824,6 +5788,8 @@ snapshots: slice-ansi: 5.0.0 string-width: 7.2.0 + cli-width@3.0.0: {} + cli-width@4.1.0: {} cliui@7.0.4: @@ -5838,6 +5804,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone@1.0.4: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -5858,6 +5826,26 @@ snapshots: commander@13.1.0: {} + commitizen@4.3.1(@types/node@18.19.80)(typescript@5.8.2): + dependencies: + cachedir: 2.3.0 + cz-conventional-changelog: 3.3.0(@types/node@18.19.80)(typescript@5.8.2) + dedent: 0.7.0 + detect-indent: 6.1.0 + find-node-modules: 2.1.3 + find-root: 1.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + inquirer: 8.2.5 + is-utf8: 0.2.1 + lodash: 4.17.21 + minimist: 1.2.7 + strip-bom: 4.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + compare-func@2.0.0: dependencies: array-ify: 1.0.0 @@ -5889,6 +5877,8 @@ snapshots: meow: 13.2.0 semver: 7.7.1 + conventional-commit-types@3.0.0: {} + conventional-commits-filter@5.0.0: {} conventional-commits-parser@5.0.0: @@ -5941,6 +5931,20 @@ snapshots: '@asamuzakjp/css-color': 3.1.1 rrweb-cssom: 0.8.0 + cz-conventional-changelog@3.3.0(@types/node@18.19.80)(typescript@5.8.2): + dependencies: + chalk: 2.4.2 + commitizen: 4.3.1(@types/node@18.19.80)(typescript@5.8.2) + conventional-commit-types: 3.0.0 + lodash.map: 4.6.0 + longest: 2.0.1 + word-wrap: 1.2.5 + optionalDependencies: + '@commitlint/load': 19.8.0(@types/node@18.19.80)(typescript@5.8.2) + transitivePeerDependencies: + - '@types/node' + - typescript + dargs@8.1.0: {} data-urls@5.0.0: @@ -5976,6 +5980,8 @@ snapshots: decimal.js@10.5.0: {} + dedent@0.7.0: {} + deep-eql@5.0.2: {} deep-extend@0.6.0: {} @@ -5984,6 +5990,10 @@ snapshots: deepmerge@4.3.1: {} + defaults@1.0.4: + dependencies: + clone: 1.0.4 + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -6011,6 +6021,8 @@ snapshots: dequal@2.0.3: {} + detect-file@1.0.0: {} + detect-indent@6.1.0: {} dir-glob@3.0.1: @@ -6059,11 +6071,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 - enquirer@2.4.1: - dependencies: - ansi-colors: 4.1.3 - strip-ansi: 6.0.1 - entities@4.5.0: {} env-ci@11.1.0: @@ -6340,8 +6347,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 - esprima@4.0.1: {} - esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -6401,9 +6406,11 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.1 - expect-type@1.2.0: {} + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 - extendable-error@0.1.7: {} + expect-type@1.2.0: {} external-editor@3.1.0: dependencies: @@ -6443,6 +6450,10 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -6457,6 +6468,13 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-node-modules@2.1.3: + dependencies: + findup-sync: 4.0.0 + merge: 2.1.1 + + find-root@1.1.0: {} + find-up-simple@1.0.1: {} find-up@2.1.0: @@ -6467,11 +6485,6 @@ snapshots: dependencies: locate-path: 3.0.0 - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -6488,6 +6501,13 @@ snapshots: semver-regex: 4.0.5 super-regex: 1.0.0 + findup-sync@4.0.0: + dependencies: + detect-file: 1.0.0 + is-glob: 4.0.3 + micromatch: 4.0.8 + resolve-dir: 1.0.1 + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -6537,17 +6557,12 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 - fs-extra@7.0.1: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fs-extra@8.1.0: + fs-extra@9.1.0: dependencies: + at-least-node: 1.0.0 graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 + jsonfile: 6.1.0 + universalify: 2.0.1 fs.realpath@1.0.0: {} @@ -6669,6 +6684,20 @@ snapshots: dependencies: ini: 4.1.1 + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + globals@14.0.0: {} globalthis@1.0.4: @@ -6741,6 +6770,10 @@ snapshots: highlight.js@10.7.3: {} + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + hook-std@3.0.0: {} hosted-git-info@2.8.9: {} @@ -6771,8 +6804,6 @@ snapshots: transitivePeerDependencies: - supports-color - human-id@4.1.1: {} - human-signals@2.1.0: {} human-signals@5.0.0: {} @@ -6793,6 +6824,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.3: {} @@ -6837,6 +6870,26 @@ snapshots: ini@4.1.1: {} + ini@5.0.0: {} + + inquirer@8.2.5: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 7.0.0 + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -6919,6 +6972,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-interactive@1.0.0: {} + is-map@2.0.3: {} is-node-process@1.2.0: {} @@ -6964,10 +7019,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-subdir@1.2.0: - dependencies: - better-path-resolve: 1.0.0 - is-symbol@1.1.1: dependencies: call-bound: 1.0.4 @@ -6982,8 +7033,12 @@ snapshots: dependencies: which-typed-array: 1.1.19 + is-unicode-supported@0.1.0: {} + is-unicode-supported@2.1.0: {} + is-utf8@0.2.1: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -7027,11 +7082,6 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -7080,10 +7130,6 @@ snapshots: dependencies: minimist: 1.2.8 - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 - jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -7146,10 +7192,6 @@ snapshots: p-locate: 3.0.0 path-exists: 3.0.0 - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -7172,6 +7214,8 @@ snapshots: lodash.kebabcase@4.1.1: {} + lodash.map@4.6.0: {} + lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} @@ -7188,6 +7232,11 @@ snapshots: lodash@4.17.21: {} + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + log-update@6.1.0: dependencies: ansi-escapes: 7.0.0 @@ -7196,6 +7245,8 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + longest@2.0.1: {} + loupe@3.1.3: {} lru-cache@10.4.3: {} @@ -7231,6 +7282,8 @@ snapshots: merge2@1.4.1: {} + merge@2.1.1: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -7262,14 +7315,14 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minimist@1.2.7: {} + minimist@1.2.8: {} minipass@7.1.2: {} module-details-from-path@1.0.3: {} - mri@1.2.0: {} - mrmime@2.0.1: {} ms@2.1.3: {} @@ -7299,6 +7352,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + mute-stream@0.0.8: {} + mute-stream@2.0.0: {} mz@2.7.0: @@ -7343,6 +7398,12 @@ snapshots: semver: 7.7.1 validate-npm-package-license: 3.0.4 + normalize-package-data@7.0.0: + dependencies: + hosted-git-info: 8.0.2 + semver: 7.7.1 + validate-npm-package-license: 3.0.4 + normalize-url@8.0.1: {} npm-run-path@4.0.1: @@ -7413,6 +7474,21 @@ snapshots: dependencies: mimic-function: 5.0.1 + openai@4.87.3(encoding@0.1.13)(ws@8.18.1)(zod@3.24.2): + dependencies: + '@types/node': 18.19.80 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + optionalDependencies: + ws: 8.18.1 + zod: 3.24.2 + transitivePeerDependencies: + - encoding + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7422,9 +7498,19 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - os-tmpdir@1.0.2: {} + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 - outdent@0.5.0: {} + os-tmpdir@1.0.2: {} outvariant@1.4.3: {} @@ -7438,10 +7524,6 @@ snapshots: p-each-series@3.0.0: {} - p-filter@2.1.0: - dependencies: - p-map: 2.1.0 - p-filter@4.1.0: dependencies: p-map: 7.0.3 @@ -7472,10 +7554,6 @@ snapshots: dependencies: p-limit: 2.3.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -7484,8 +7562,6 @@ snapshots: dependencies: p-limit: 4.0.0 - p-map@2.1.0: {} - p-map@4.0.0: dependencies: aggregate-error: 3.1.0 @@ -7530,6 +7606,8 @@ snapshots: parse-ms@4.0.0: {} + parse-passwd@1.0.0: {} + parse5-htmlparser2-tree-adapter@6.0.1: dependencies: parse5: 6.0.1 @@ -7598,8 +7676,6 @@ snapshots: pify@3.0.0: {} - pify@4.0.1: {} - pkg-conf@2.1.0: dependencies: find-up: 2.1.0 @@ -7641,8 +7717,6 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@2.8.8: {} - prettier@3.5.3: {} pretty-format@27.5.1: @@ -7703,13 +7777,6 @@ snapshots: type-fest: 4.37.0 unicorn-magic: 0.1.0 - read-yaml-file@1.1.0: - dependencies: - graceful-fs: 4.2.11 - js-yaml: 3.14.1 - pify: 4.0.1 - strip-bom: 3.0.0 - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -7720,6 +7787,12 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -7760,6 +7833,11 @@ snapshots: requires-port@1.0.0: {} + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -7772,6 +7850,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + restore-cursor@5.1.0: dependencies: onetime: 7.0.0 @@ -7821,10 +7904,16 @@ snapshots: rrweb-cssom@0.8.0: {} + run-async@2.4.1: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -8027,11 +8116,6 @@ snapshots: spawn-error-forwarder@1.0.0: {} - spawndamnit@3.0.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -8052,8 +8136,6 @@ snapshots: split2@4.2.0: {} - sprintf-js@1.0.3: {} - stable-hash@0.0.4: {} stackback@0.0.2: {} @@ -8126,6 +8208,8 @@ snapshots: strip-bom@3.0.0: {} + strip-bom@4.0.0: {} + strip-final-newline@2.0.0: {} strip-final-newline@3.0.0: {} @@ -8184,8 +8268,6 @@ snapshots: type-fest: 2.19.0 unique-string: 3.0.0 - term-size@2.2.1: {} - text-extensions@2.4.0: {} thenify-all@1.6.0: @@ -8261,6 +8343,8 @@ snapshots: dependencies: typescript: 5.8.2 + ts-deepmerge@7.0.2: {} + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -8359,8 +8443,6 @@ snapshots: universal-user-agent@7.0.2: {} - universalify@0.1.2: {} - universalify@0.2.0: {} universalify@2.0.1: {} @@ -8461,6 +8543,10 @@ snapshots: dependencies: xml-name-validator: 5.0.0 + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + web-streams-polyfill@4.0.0-beta.3: {} webidl-conversions@3.0.1: {} @@ -8524,6 +8610,10 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/scripts/verify-release-config.js b/scripts/verify-release-config.js index c7d1c5d..85906eb 100755 --- a/scripts/verify-release-config.js +++ b/scripts/verify-release-config.js @@ -25,19 +25,34 @@ if (!hasSemanticReleaseMonorepo) { process.exit(1); } -console.log('Checking root .releaserc.json...'); -// Check root .releaserc.json -const rootReleaseRc = JSON.parse( - fs.readFileSync(path.join(ROOT_DIR, '.releaserc.json'), 'utf8'), -); -if ( - !rootReleaseRc.extends || - rootReleaseRc.extends !== 'semantic-release-monorepo' -) { - console.error( - '❌ Root .releaserc.json does not extend semantic-release-monorepo', +console.log('Checking if root package is private...'); +// Only check for root .releaserc.json if the root package is not private +if (!rootPackageJson.private) { + console.log('Root package is not private, checking root .releaserc.json...'); + try { + // Check root .releaserc.json + const rootReleaseRc = JSON.parse( + fs.readFileSync(path.join(ROOT_DIR, '.releaserc.json'), 'utf8'), + ); + if ( + !rootReleaseRc.extends || + rootReleaseRc.extends !== 'semantic-release-monorepo' + ) { + console.error( + '❌ Root .releaserc.json does not extend semantic-release-monorepo', + ); + process.exit(1); + } + } catch (error) { + console.error( + '❌ Root .releaserc.json is missing but required for non-private root packages', + ); + process.exit(1); + } +} else { + console.log( + 'Root package is private, skipping root .releaserc.json check...', ); - process.exit(1); } console.log('Checking packages...');