Compare commits

...

14 Commits

Author SHA1 Message Date
Ralph Khreish
10e5591bf8 chore: apply requested changes 2025-10-02 15:35:13 +02:00
claude[bot]
ae4c4d9b93 fix: make tag listing table use dynamic column widths to prevent truncation
- Replace hardcoded colWidths with terminal-width-based calculation
- Tag Name column now gets 70% width (normal) or 25% width (metadata mode)
- Add wordWrap support for better handling of long tag names
- Fixes issue where tag names >20 chars were truncated with ellipsis
- Users can now see full tag names for use with use-tag command

Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
2025-10-02 15:32:47 +02:00
Ralph Khreish
0747f1c772 Merge pull request #1265 from eyaltoledano/ralph/chore/update.from.main 2025-10-02 15:31:42 +02:00
Ralph Khreish
ffe24a2e35 Merge remote-tracking branch 'origin/main' into ralph/chore/update.from.main 2025-10-02 15:11:24 +02:00
Ralph Khreish
604b94baa9 chore: replace dotenv-mono with dotenv and try to fix env variables (#1261) 2025-10-02 11:52:25 +02:00
Ralph Khreish
2ea4bb6a81 chore: fix CI 2025-09-30 10:41:43 +02:00
Ralph Khreish
3e96387715 chore: fix extension CI 2025-09-30 10:41:43 +02:00
Ralph Khreish
100c3dc47d chore: apply requested changes 2025-09-30 10:41:43 +02:00
Ralph Khreish
986ac117ae feat: update grok-cli ai sdk provider to v5 (#1252) 2025-09-30 10:41:43 +02:00
tommy-ca
18aa416035 feat: Claude Code AI SDK v5 Integration (#1114)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-09-30 10:41:43 +02:00
github-actions[bot]
3b3dbabed1 Version Packages (#1255)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-09-27 08:56:38 +02:00
Ralph Khreish
af53525cbc fix: handle subtasks in getTask method (#1254)
Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-09-26 20:58:15 +02:00
Joe Danziger
0079b7defd feat: Add Cursor IDE custom slash commands support (#1215) 2025-09-26 19:21:16 +02:00
Ralph Khreish
0b2c6967c4 fix: improve subtask & parent task management (#1251) 2025-09-26 11:04:38 +02:00
74 changed files with 6506 additions and 10312 deletions

View File

@@ -0,0 +1,7 @@
---
"task-master-ai": minor
---
Add Cursor IDE custom slash command support
Expose Task Master commands as Cursor slash commands by copying assets/claude/commands to .cursor/commands on profile add and cleaning up on remove.

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": patch
---
Change parent task back to "pending" when all subtasks are in "pending" state

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": minor
---
Added api keys page on docs website: docs.task-master.dev/getting-started/api-keys

View File

@@ -0,0 +1,10 @@
---
"task-master-ai": minor
---
Move to AI SDK v5:
- Works better with claude-code and gemini-cli as ai providers
- Improved openai model family compatibility
- Migrate ollama provider to v2
- Closes #1223, #1013, #1161, #1174

View File

@@ -0,0 +1,5 @@
---
"task-master-ai": minor
---
Upgrade grok-cli ai provider to ai sdk v5

View File

@@ -41,8 +41,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- name: Install Extension Dependencies
working-directory: apps/extension
- name: Install Monorepo Dependencies
run: npm ci
timeout-minutes: 5
@@ -68,7 +67,6 @@ jobs:
${{ runner.os }}-node-
- name: Install if cache miss
working-directory: apps/extension
run: npm ci
timeout-minutes: 3
@@ -100,7 +98,6 @@ jobs:
${{ runner.os }}-node-
- name: Install if cache miss
working-directory: apps/extension
run: npm ci
timeout-minutes: 3

View File

@@ -31,8 +31,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- name: Install Extension Dependencies
working-directory: apps/extension
- name: Install Monorepo Dependencies
run: npm ci
timeout-minutes: 5

View File

@@ -1,5 +1,12 @@
# task-master-ai
## 0.27.3
### Patch Changes
- [#1254](https://github.com/eyaltoledano/claude-task-master/pull/1254) [`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fixed issue where `tm show` command could not find subtasks using dotted notation IDs (e.g., '8.1').
- The command now properly searches within parent task subtasks and returns the correct subtask information.
## 0.27.2
### Patch Changes

View File

@@ -35,7 +35,7 @@
"@types/inquirer": "^9.0.3",
"@types/node": "^22.10.5",
"tsx": "^4.20.4",
"typescript": "^5.7.3",
"typescript": "^5.9.2",
"vitest": "^2.1.8"
},
"engines": {

View File

@@ -32,6 +32,7 @@
"getting-started/quick-start/execute-quick"
]
},
"getting-started/api-keys",
"getting-started/faq",
"getting-started/contribute"
]

View File

@@ -0,0 +1,267 @@
# API Keys Configuration
Task Master supports multiple AI providers through environment variables. This page lists all available API keys and their configuration requirements.
## Required API Keys
> **Note**: At least one required API key must be configured for Task Master to function.
>
> "Required: Yes" below means "required to use that specific provider," not "required globally." You only need at least one provider configured.
### ANTHROPIC_API_KEY (Recommended)
- **Provider**: Anthropic Claude models
- **Format**: `sk-ant-api03-...`
- **Required**: ✅ **Yes**
- **Models**: Claude 3.5 Sonnet, Claude 3 Haiku, Claude 3 Opus
- **Get Key**: [Anthropic Console](https://console.anthropic.com/)
```bash
ANTHROPIC_API_KEY="sk-ant-api03-your-key-here"
```
### PERPLEXITY_API_KEY (Highly Recommended for Research)
- **Provider**: Perplexity AI (Research features)
- **Format**: `pplx-...`
- **Required**: ✅ **Yes**
- **Purpose**: Enables research-backed task expansions and updates
- **Models**: Perplexity Sonar models
- **Get Key**: [Perplexity API](https://www.perplexity.ai/settings/api)
```bash
PERPLEXITY_API_KEY="pplx-your-key-here"
```
### OPENAI_API_KEY
- **Provider**: OpenAI GPT models
- **Format**: `sk-proj-...` or `sk-...`
- **Required**: ✅ **Yes**
- **Models**: GPT-4, GPT-4 Turbo, GPT-3.5 Turbo, O1 models
- **Get Key**: [OpenAI Platform](https://platform.openai.com/api-keys)
```bash
OPENAI_API_KEY="sk-proj-your-key-here"
```
### GOOGLE_API_KEY
- **Provider**: Google Gemini models
- **Format**: Various formats
- **Required**: ✅ **Yes**
- **Models**: Gemini Pro, Gemini Flash, Gemini Ultra
- **Get Key**: [Google AI Studio](https://aistudio.google.com/app/apikey)
- **Alternative**: Use `GOOGLE_APPLICATION_CREDENTIALS` for service account (Google Vertex)
```bash
GOOGLE_API_KEY="your-google-api-key-here"
```
### GROQ_API_KEY
- **Provider**: Groq (High-performance inference)
- **Required**: ✅ **Yes**
- **Models**: Llama models, Mixtral models (via Groq)
- **Get Key**: [Groq Console](https://console.groq.com/keys)
```bash
GROQ_API_KEY="your-groq-key-here"
```
### OPENROUTER_API_KEY
- **Provider**: OpenRouter (Multiple model access)
- **Required**: ✅ **Yes**
- **Models**: Access to various models through single API
- **Get Key**: [OpenRouter](https://openrouter.ai/keys)
```bash
OPENROUTER_API_KEY="your-openrouter-key-here"
```
### AZURE_OPENAI_API_KEY
- **Provider**: Azure OpenAI Service
- **Required**: ✅ **Yes**
- **Requirements**: Also requires `AZURE_OPENAI_ENDPOINT` configuration
- **Models**: GPT models via Azure
- **Get Key**: [Azure Portal](https://portal.azure.com/)
```bash
AZURE_OPENAI_API_KEY="your-azure-key-here"
```
### XAI_API_KEY
- **Provider**: xAI (Grok) models
- **Required**: ✅ **Yes**
- **Models**: Grok models
- **Get Key**: [xAI Console](https://console.x.ai/)
```bash
XAI_API_KEY="your-xai-key-here"
```
## Optional API Keys
> **Note**: These API keys are optional - providers will work without them or use alternative authentication methods.
### AWS_ACCESS_KEY_ID (Bedrock)
- **Provider**: AWS Bedrock
- **Required**: ❌ **No** (uses AWS credential chain)
- **Models**: Claude models via AWS Bedrock
- **Authentication**: Uses AWS credential chain (profiles, IAM roles, etc.)
- **Get Key**: [AWS Console](https://console.aws.amazon.com/iam/)
```bash
# Optional - AWS credential chain is preferred
AWS_ACCESS_KEY_ID="your-aws-access-key"
AWS_SECRET_ACCESS_KEY="your-aws-secret-key"
```
### CLAUDE_CODE_API_KEY
- **Provider**: Claude Code CLI
- **Required**: ❌ **No** (uses OAuth tokens)
- **Purpose**: Integration with local Claude Code CLI
- **Authentication**: Uses OAuth tokens, no API key needed
```bash
# Not typically needed
CLAUDE_CODE_API_KEY="not-usually-required"
```
### GEMINI_API_KEY
- **Provider**: Gemini CLI
- **Required**: ❌ **No** (uses OAuth authentication)
- **Purpose**: Integration with Gemini CLI
- **Authentication**: Primarily uses OAuth via CLI, API key is optional
```bash
# Optional - OAuth via CLI is preferred
GEMINI_API_KEY="your-gemini-key-here"
```
### GROK_CLI_API_KEY
- **Provider**: Grok CLI
- **Required**: ❌ **No** (can use CLI config)
- **Purpose**: Integration with Grok CLI
- **Authentication**: Can use Grok CLI's own config file
```bash
# Optional - CLI config is preferred
GROK_CLI_API_KEY="your-grok-cli-key"
```
### OLLAMA_API_KEY
- **Provider**: Ollama (Local/Remote)
- **Required**: ❌ **No** (local installation doesn't need key)
- **Purpose**: For remote Ollama servers that require authentication
- **Requirements**: Only needed for remote servers with authentication
- **Note**: Not needed for local Ollama installations
```bash
# Only needed for remote Ollama servers
OLLAMA_API_KEY="your-ollama-api-key-here"
```
### GITHUB_API_KEY
- **Provider**: GitHub (Import/Export features)
- **Format**: `ghp_...` or `github_pat_...`
- **Required**: ❌ **No** (for GitHub features only)
- **Purpose**: GitHub import/export features
- **Get Key**: [GitHub Settings](https://github.com/settings/tokens)
```bash
GITHUB_API_KEY="ghp-your-github-key-here"
```
## Configuration Methods
### Method 1: Environment File (.env)
Create a `.env` file in your project root:
```bash
# Copy from .env.example
cp .env.example .env
# Edit with your keys
vim .env
```
### Method 2: System Environment Variables
```bash
export ANTHROPIC_API_KEY="your-key-here"
export PERPLEXITY_API_KEY="your-key-here"
# ... other keys
```
### Method 3: MCP Server Configuration
For Claude Code integration, configure keys in `.mcp.json`:
```json
{
"mcpServers": {
"task-master-ai": {
"command": "npx",
"args": ["-y", "task-master-ai"],
"env": {
"ANTHROPIC_API_KEY": "your-key-here",
"PERPLEXITY_API_KEY": "your-key-here",
"OPENAI_API_KEY": "your-key-here"
}
}
}
}
```
## Key Requirements
### Minimum Requirements
- **At least one** AI provider key is required
- **ANTHROPIC_API_KEY** is recommended as the primary provider
- **PERPLEXITY_API_KEY** is highly recommended for research features
### Provider-Specific Requirements
- **Azure OpenAI**: Requires both `AZURE_OPENAI_API_KEY` and `AZURE_OPENAI_ENDPOINT` configuration
- **Google Vertex**: Requires `VERTEX_PROJECT_ID` and `VERTEX_LOCATION` environment variables
- **AWS Bedrock**: Uses AWS credential chain (profiles, IAM roles, etc.) instead of API keys
- **Ollama**: Only needs API key for remote servers with authentication
- **CLI Providers**: Gemini CLI, Grok CLI, and Claude Code use OAuth/CLI config instead of API keys
## Model Configuration
After setting up API keys, configure which models to use:
```bash
# Interactive model setup
task-master models --setup
# Set specific models
task-master models --set-main claude-3-5-sonnet-20241022
task-master models --set-research perplexity-llama-3.1-sonar-large-128k-online
task-master models --set-fallback gpt-4o-mini
```
## Security Best Practices
1. **Never commit API keys** to version control
2. **Use .env files** and add them to `.gitignore`
3. **Rotate keys regularly** especially if compromised
4. **Use minimal permissions** for service accounts
5. **Monitor usage** to detect unauthorized access
## Troubleshooting
### Key Validation
```bash
# Check if keys are properly configured
task-master models
# Test specific provider
task-master add-task --prompt="test task" --model=claude-3-5-sonnet-20241022
```
### Common Issues
- **Invalid key format**: Check the expected format for each provider
- **Insufficient permissions**: Ensure keys have necessary API access
- **Rate limits**: Some providers have usage limits
- **Regional restrictions**: Some models may not be available in all regions
### Getting Help
If you encounter issues with API key configuration:
- Check the [FAQ](/getting-started/faq) for common solutions
- Join our [Discord community](https://discord.gg/fWJkU7rf) for support
- Report issues on [GitHub](https://github.com/eyaltoledano/claude-task-master/issues)

View File

@@ -1,5 +1,12 @@
# Change Log
## 0.25.4
### Patch Changes
- Updated dependencies [[`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d)]:
- task-master-ai@0.27.3
## 0.25.3
### Patch Changes

View File

@@ -3,7 +3,7 @@
"private": true,
"displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.25.3",
"version": "0.25.4",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {
@@ -240,7 +240,7 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
"task-master-ai": "0.27.2"
"task-master-ai": "*"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
@@ -276,7 +276,8 @@
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.1",
"tailwindcss": "4.1.11",
"typescript": "^5.7.3"
"typescript": "^5.9.2",
"@tm/core": "*"
},
"overrides": {
"glob@<8": "^10.4.5",

View File

@@ -2,7 +2,7 @@
"name": "task-master-hamster",
"displayName": "Taskmaster AI",
"description": "A visual Kanban board interface for Taskmaster projects in VS Code",
"version": "0.23.1",
"version": "0.25.3",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {

View File

@@ -5,7 +5,6 @@
"outDir": "out",
"lib": ["ES2022", "DOM"],
"sourceMap": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */,
"moduleResolution": "Node",
"esModuleInterop": true,
@@ -21,8 +20,10 @@
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/lib/*": ["./src/lib/*"],
"@tm/core": ["../core/src"]
"@tm/core": ["../../packages/tm-core/src/index.ts"],
"@tm/core/*": ["../../packages/tm-core/src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", ".vscode-test", "out", "dist"]
}

View File

@@ -0,0 +1,231 @@
# TODO: Move to apps/docs inside our documentation website
# Claude Code Integration Guide
This guide covers how to use Task Master with Claude Code AI SDK integration for enhanced AI-powered development workflows.
## Overview
Claude Code integration allows Task Master to leverage the Claude Code CLI for AI operations without requiring direct API keys. The integration uses OAuth tokens managed by the Claude Code CLI itself.
## Authentication Setup
The Claude Code provider uses token authentication managed by the Claude Code CLI.
### Prerequisites
1. **Install Claude Code CLI** (if not already installed):
```bash
# Installation method depends on your system
# Follow Claude Code documentation for installation
```
2. **Set up OAuth token** using Claude Code CLI:
```bash
claude setup-token
```
This command will:
- Guide you through OAuth authentication
- Store the token securely for CLI usage
- Enable Task Master to use Claude Code without manual API key configuration
### Authentication Priority
Task Master will attempt authentication in this order:
1. **Environment Variable** (optional): `CLAUDE_CODE_OAUTH_TOKEN`
- Useful for CI/CD environments or when you want to override the default token
- Not required if you've set up the CLI token
2. **Claude Code CLI Token** (recommended): Token managed by `claude setup-token`
- Automatically used when available
- Most convenient for local development
3. **Fallback**: Error if neither is available
## Configuration
### Basic Configuration
Add Claude Code to your Task Master configuration:
```javascript
// In your .taskmaster/config.json or via task-master models command
{
"models": {
"main": "claude-code:sonnet", // Use Claude Code with Sonnet
"research": "perplexity-llama-3.1-sonar-large-128k-online",
"fallback": "claude-code:opus" // Use Claude Code with Opus as fallback
}
}
```
### Supported Models
- `claude-code:sonnet` - Claude 3.5 Sonnet via Claude Code CLI
- `claude-code:opus` - Claude 3 Opus via Claude Code CLI
### Environment Variables (Optional)
While not required, you can optionally set:
```bash
export CLAUDE_CODE_OAUTH_TOKEN="your_oauth_token_here"
```
This is only needed in specific scenarios like:
- CI/CD pipelines
- Docker containers
- When you want to use a different token than the CLI default
## Usage Examples
### Basic Task Operations
```bash
# Use Claude Code for task operations
task-master add-task --prompt="Implement user authentication system" --research
task-master expand --id=1 --research
task-master update-task --id=1.1 --prompt="Add JWT token validation"
```
### Model Configuration Commands
```bash
# Set Claude Code as main model
task-master models --set-main claude-code:sonnet
# Use interactive setup
task-master models --setup
# Then select "claude-code" from the provider list
```
## Troubleshooting
### Common Issues
#### 1. "Claude Code CLI not available" Error
**Problem**: Task Master cannot connect to Claude Code CLI.
**Solutions**:
- Ensure Claude Code CLI is installed and in your PATH
- Run `claude setup-token` to configure authentication
- Verify Claude Code CLI works: `claude --help`
#### 2. Authentication Failures
**Problem**: Token authentication is failing.
**Solutions**:
- Re-run `claude setup-token` to refresh your OAuth token
- Check if your token has expired
- Verify Claude Code CLI can authenticate: try a simple `claude` command
#### 3. Model Not Available
**Problem**: Specified Claude Code model is not supported.
**Solutions**:
- Use supported models: `sonnet` or `opus`
- Check model availability: `task-master models --list`
- Verify your Claude Code CLI has access to the requested model
### Debug Steps
1. **Test Claude Code CLI directly**:
```bash
claude --help
# Should show help without errors
```
2. **Test authentication**:
```bash
claude setup-token --verify
# Should confirm token is valid
```
3. **Test Task Master integration**:
```bash
task-master models --test claude-code:sonnet
# Should successfully connect and test the model
```
4. **Check logs**:
- Task Master logs will show detailed error messages
- Use `--verbose` flag for more detailed output
### Environment-Specific Configuration
#### Docker/Containers
When running in Docker, you'll need to:
1. Install Claude Code CLI in your container
2. Set up authentication via environment variable:
```dockerfile
ENV CLAUDE_CODE_OAUTH_TOKEN="your_token_here"
```
#### CI/CD Pipelines
For automated environments:
1. Set up a service account token or use environment variables
2. Ensure Claude Code CLI is available in the pipeline environment
3. Configure authentication before running Task Master commands
## Integration with AI SDK
Task Master's Claude Code integration uses the official `ai-sdk-provider-claude-code` package, providing:
- **Streaming Support**: Real-time token streaming for interactive experiences
- **Full AI SDK Compatibility**: Works with generateText, streamText, and other AI SDK functions
- **Automatic Error Handling**: Graceful degradation when Claude Code is unavailable
- **Type Safety**: Full TypeScript support with proper type definitions
### Example AI SDK Usage
```javascript
import { generateText } from 'ai';
import { ClaudeCodeProvider } from './src/ai-providers/claude-code.js';
const provider = new ClaudeCodeProvider();
const client = provider.getClient();
const result = await generateText({
model: client('sonnet'),
messages: [
{ role: 'user', content: 'Hello Claude!' }
]
});
console.log(result.text);
```
## Security Notes
- OAuth tokens are managed securely by Claude Code CLI
- No API keys need to be stored in your project files
- Tokens are automatically refreshed by the Claude Code CLI
- Environment variables should only be used in secure environments
## Getting Help
If you encounter issues:
1. Check the Claude Code CLI documentation
2. Verify your authentication setup with `claude setup-token --verify`
3. Review Task Master logs for detailed error messages
4. Open an issue with both Task Master and Claude Code version information

11725
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "task-master-ai",
"version": "0.27.2",
"version": "0.27.3",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js",
"type": "module",
@@ -17,7 +17,7 @@
"turbo:build": "turbo build",
"turbo:typecheck": "turbo typecheck",
"build:build-config": "npm run build -w @tm/build-config",
"test": "node --experimental-vm-modules node_modules/.bin/jest",
"test": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/.bin/jest",
"test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit",
"test:integration": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=integration",
"test:fails": "node --experimental-vm-modules node_modules/.bin/jest --onlyFailures",
@@ -52,23 +52,26 @@
"author": "Eyal Toledano",
"license": "MIT WITH Commons-Clause",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^2.2.9",
"@ai-sdk/anthropic": "^1.2.10",
"@ai-sdk/azure": "^1.3.17",
"@ai-sdk/google": "^1.2.13",
"@ai-sdk/google-vertex": "^2.2.23",
"@ai-sdk/groq": "^1.2.9",
"@ai-sdk/mistral": "^1.2.7",
"@ai-sdk/openai": "^1.3.20",
"@ai-sdk/perplexity": "^1.1.7",
"@ai-sdk/xai": "^1.2.15",
"@anthropic-ai/sdk": "^0.39.0",
"@aws-sdk/credential-providers": "^3.817.0",
"@ai-sdk/amazon-bedrock": "^3.0.23",
"@ai-sdk/anthropic": "^2.0.18",
"@ai-sdk/azure": "^2.0.34",
"@ai-sdk/google": "^2.0.16",
"@ai-sdk/google-vertex": "^3.0.29",
"@ai-sdk/groq": "^2.0.21",
"@ai-sdk/mistral": "^2.0.16",
"@ai-sdk/openai": "^2.0.34",
"@ai-sdk/perplexity": "^2.0.10",
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.10",
"@ai-sdk/xai": "^2.0.22",
"@aws-sdk/credential-providers": "^3.895.0",
"@inquirer/search": "^3.0.15",
"@openrouter/ai-sdk-provider": "^0.4.5",
"@openrouter/ai-sdk-provider": "^1.2.0",
"@streamparser/json": "^0.0.22",
"@supabase/supabase-js": "^2.57.4",
"ai": "^4.3.10",
"ai": "^5.0.51",
"ai-sdk-provider-claude-code": "^1.1.4",
"ai-sdk-provider-gemini-cli": "^1.1.1",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"boxen": "^8.0.1",
@@ -78,7 +81,7 @@
"cli-table3": "^0.6.5",
"commander": "^12.1.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"dotenv": "^16.6.1",
"express": "^4.21.2",
"fastmcp": "^3.5.0",
"figlet": "^1.8.0",
@@ -93,17 +96,14 @@
"lru-cache": "^10.2.0",
"marked": "^15.0.12",
"marked-terminal": "^7.3.0",
"ollama-ai-provider": "^1.2.0",
"openai": "^4.89.0",
"ollama-ai-provider-v2": "^1.3.1",
"ora": "^8.2.0",
"uuid": "^11.1.0",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.24.5"
"zod": "^4.1.11"
},
"optionalDependencies": {
"@anthropic-ai/claude-code": "^1.0.88",
"@biomejs/cli-linux-x64": "^1.9.4",
"ai-sdk-provider-gemini-cli": "^0.1.3"
"@biomejs/cli-linux-x64": "^1.9.4"
},
"engines": {
"node": ">=18.0.0"
@@ -127,12 +127,12 @@
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.28.1",
"@manypkg/cli": "^0.25.1",
"@tm/ai-sdk-provider-grok-cli": "*",
"@tm/cli": "*",
"@types/jest": "^29.5.14",
"@types/marked-terminal": "^6.1.1",
"concurrently": "^9.2.1",
"cross-env": "^10.0.0",
"dotenv-mono": "^1.5.1",
"execa": "^8.0.1",
"jest": "^29.7.0",
"jest-environment-node": "^29.7.0",
@@ -142,7 +142,7 @@
"ts-jest": "^29.4.2",
"tsdown": "^0.15.2",
"tsx": "^4.20.4",
"turbo": "^2.5.6",
"typescript": "^5.7.3"
"turbo": "2.5.6",
"typescript": "^5.9.2"
}
}

View File

@@ -0,0 +1,165 @@
# AI SDK Provider for Grok CLI
A provider for the [AI SDK](https://sdk.vercel.ai) that integrates with [Grok CLI](https://docs.x.ai/api) for accessing xAI's Grok language models.
## Features
-**AI SDK v5 Compatible** - Full support for the latest AI SDK interfaces
-**Streaming & Non-streaming** - Both generation modes supported
-**Error Handling** - Comprehensive error handling with retry logic
-**Type Safety** - Full TypeScript support with proper type definitions
-**JSON Mode** - Automatic JSON extraction from responses
-**Abort Signals** - Proper cancellation support
## Installation
```bash
npm install @tm/ai-sdk-provider-grok-cli
# or
yarn add @tm/ai-sdk-provider-grok-cli
```
## Prerequisites
1. Install the Grok CLI:
```bash
npm install -g grok-cli
# or follow xAI's installation instructions
```
2. Set up authentication:
```bash
export GROK_CLI_API_KEY="your-api-key"
# or configure via grok CLI: grok config set api-key your-key
```
## Usage
### Basic Usage
```typescript
import { grokCli } from '@tm/ai-sdk-provider-grok-cli';
import { generateText } from 'ai';
const result = await generateText({
model: grokCli('grok-3-latest'),
prompt: 'Write a haiku about TypeScript'
});
console.log(result.text);
```
### Streaming
```typescript
import { grokCli } from '@tm/ai-sdk-provider-grok-cli';
import { streamText } from 'ai';
const { textStream } = await streamText({
model: grokCli('grok-4-latest'),
prompt: 'Explain quantum computing'
});
for await (const delta of textStream) {
process.stdout.write(delta);
}
```
### JSON Mode
```typescript
import { grokCli } from '@tm/ai-sdk-provider-grok-cli';
import { generateObject } from 'ai';
import { z } from 'zod';
const result = await generateObject({
model: grokCli('grok-3-latest'),
schema: z.object({
name: z.string(),
age: z.number(),
hobbies: z.array(z.string())
}),
prompt: 'Generate a person profile'
});
console.log(result.object);
```
## Supported Models
- `grok-3-latest` - Grok 3 (latest version)
- `grok-4-latest` - Grok 4 (latest version)
- `grok-4` - Grok 4 (stable)
- Custom model strings supported
## Configuration
### Provider Settings
```typescript
import { createGrokCli } from '@tm/ai-sdk-provider-grok-cli';
const grok = createGrokCli({
apiKey: 'your-api-key', // Optional if set via env/CLI
timeout: 120000, // 2 minutes default
workingDirectory: '/path/to/project', // Optional
baseURL: 'https://api.x.ai' // Optional
});
```
### Model Settings
```typescript
const model = grok('grok-4-latest', {
timeout: 300000, // 5 minutes for grok-4
// Other CLI-specific settings
});
```
## Error Handling
The provider includes comprehensive error handling:
```typescript
import {
isAuthenticationError,
isTimeoutError,
isInstallationError
} from '@tm/ai-sdk-provider-grok-cli';
try {
const result = await generateText({
model: grokCli('grok-4-latest'),
prompt: 'Hello!'
});
} catch (error) {
if (isAuthenticationError(error)) {
console.error('Authentication failed:', error.message);
} else if (isTimeoutError(error)) {
console.error('Request timed out:', error.message);
} else if (isInstallationError(error)) {
console.error('Grok CLI not installed or not found in PATH');
}
}
```
## Development
```bash
# Install dependencies
npm install
# Start development mode (keep running during development)
npm run dev
# Type check
npm run typecheck
# Run tests (requires build first)
NODE_ENV=production npm run build
npm test
```
**Important**: Always run `npm run dev` and keep it running during development. This ensures proper compilation and hot-reloading of TypeScript files.

View File

@@ -0,0 +1,35 @@
{
"name": "@tm/ai-sdk-provider-grok-cli",
"private": true,
"description": "AI SDK provider for Grok CLI integration",
"type": "module",
"types": "./src/index.ts",
"main": "./dist/index.js",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.10",
"jsonc-parser": "^3.3.1"
},
"devDependencies": {
"@types/node": "^22.18.6",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
},
"engines": {
"node": ">=18"
},
"keywords": ["ai", "grok", "x.ai", "cli", "language-model", "provider"],
"files": ["dist/**/*", "README.md"],
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,188 @@
/**
* Tests for error handling utilities
*/
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
import { describe, expect, it } from 'vitest';
import {
createAPICallError,
createAuthenticationError,
createInstallationError,
createTimeoutError,
getErrorMetadata,
isAuthenticationError,
isInstallationError,
isTimeoutError
} from './errors.js';
describe('createAPICallError', () => {
it('should create APICallError with metadata', () => {
const error = createAPICallError({
message: 'Test error',
code: 'TEST_ERROR',
exitCode: 1,
stderr: 'Error output',
stdout: 'Success output',
promptExcerpt: 'Test prompt',
isRetryable: true
});
expect(error).toBeInstanceOf(APICallError);
expect(error.message).toBe('Test error');
expect(error.isRetryable).toBe(true);
expect(error.url).toBe('grok-cli://command');
expect(error.data).toEqual({
code: 'TEST_ERROR',
exitCode: 1,
stderr: 'Error output',
stdout: 'Success output',
promptExcerpt: 'Test prompt'
});
});
it('should create APICallError with minimal parameters', () => {
const error = createAPICallError({
message: 'Simple error'
});
expect(error).toBeInstanceOf(APICallError);
expect(error.message).toBe('Simple error');
expect(error.isRetryable).toBe(false);
});
});
describe('createAuthenticationError', () => {
it('should create LoadAPIKeyError with custom message', () => {
const error = createAuthenticationError({
message: 'Custom auth error'
});
expect(error).toBeInstanceOf(LoadAPIKeyError);
expect(error.message).toBe('Custom auth error');
});
it('should create LoadAPIKeyError with default message', () => {
const error = createAuthenticationError({});
expect(error).toBeInstanceOf(LoadAPIKeyError);
expect(error.message).toContain('Authentication failed');
});
});
describe('createTimeoutError', () => {
it('should create APICallError for timeout', () => {
const error = createTimeoutError({
message: 'Operation timed out',
timeoutMs: 5000,
promptExcerpt: 'Test prompt'
});
expect(error).toBeInstanceOf(APICallError);
expect(error.message).toBe('Operation timed out');
expect(error.isRetryable).toBe(true);
expect(error.data).toEqual({
code: 'TIMEOUT',
promptExcerpt: 'Test prompt',
timeoutMs: 5000
});
});
});
describe('createInstallationError', () => {
it('should create APICallError for installation issues', () => {
const error = createInstallationError({
message: 'CLI not found'
});
expect(error).toBeInstanceOf(APICallError);
expect(error.message).toBe('CLI not found');
expect(error.isRetryable).toBe(false);
expect(error.url).toBe('grok-cli://installation');
});
it('should create APICallError with default message', () => {
const error = createInstallationError({});
expect(error).toBeInstanceOf(APICallError);
expect(error.message).toContain('Grok CLI is not installed');
});
});
describe('isAuthenticationError', () => {
it('should return true for LoadAPIKeyError', () => {
const error = new LoadAPIKeyError({ message: 'Auth failed' });
expect(isAuthenticationError(error)).toBe(true);
});
it('should return true for APICallError with 401 exit code', () => {
const error = new APICallError({
message: 'Unauthorized',
data: { exitCode: 401 }
});
expect(isAuthenticationError(error)).toBe(true);
});
it('should return false for other errors', () => {
const error = new Error('Generic error');
expect(isAuthenticationError(error)).toBe(false);
});
});
describe('isTimeoutError', () => {
it('should return true for timeout APICallError', () => {
const error = new APICallError({
message: 'Timeout',
data: { code: 'TIMEOUT' }
});
expect(isTimeoutError(error)).toBe(true);
});
it('should return false for other errors', () => {
const error = new APICallError({ message: 'Other error' });
expect(isTimeoutError(error)).toBe(false);
});
});
describe('isInstallationError', () => {
it('should return true for installation APICallError', () => {
const error = new APICallError({
message: 'Not installed',
url: 'grok-cli://installation'
});
expect(isInstallationError(error)).toBe(true);
});
it('should return false for other errors', () => {
const error = new APICallError({ message: 'Other error' });
expect(isInstallationError(error)).toBe(false);
});
});
describe('getErrorMetadata', () => {
it('should return metadata from APICallError', () => {
const metadata = {
code: 'TEST_ERROR',
exitCode: 1,
stderr: 'Error output'
};
const error = new APICallError({
message: 'Test error',
data: metadata
});
const result = getErrorMetadata(error);
expect(result).toEqual(metadata);
});
it('should return undefined for errors without metadata', () => {
const error = new Error('Generic error');
const result = getErrorMetadata(error);
expect(result).toBeUndefined();
});
it('should return undefined for APICallError without data', () => {
const error = new APICallError({ message: 'Test error' });
const result = getErrorMetadata(error);
expect(result).toBeUndefined();
});
});

View File

@@ -0,0 +1,187 @@
/**
* Error handling utilities for Grok CLI provider
*/
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
import type { GrokCliErrorMetadata } from './types.js';
/**
* Parameters for creating API call errors
*/
interface CreateAPICallErrorParams {
/** Error message */
message: string;
/** Error code */
code?: string;
/** Process exit code */
exitCode?: number;
/** Standard error output */
stderr?: string;
/** Standard output */
stdout?: string;
/** Excerpt of the prompt */
promptExcerpt?: string;
/** Whether the error is retryable */
isRetryable?: boolean;
}
/**
* Parameters for creating authentication errors
*/
interface CreateAuthenticationErrorParams {
/** Error message */
message?: string;
}
/**
* Parameters for creating timeout errors
*/
interface CreateTimeoutErrorParams {
/** Error message */
message: string;
/** Excerpt of the prompt */
promptExcerpt?: string;
/** Timeout in milliseconds */
timeoutMs: number;
}
/**
* Parameters for creating installation errors
*/
interface CreateInstallationErrorParams {
/** Error message */
message?: string;
}
/**
* Create an API call error with Grok CLI specific metadata
*/
export function createAPICallError({
message,
code,
exitCode,
stderr,
stdout,
promptExcerpt,
isRetryable = false
}: CreateAPICallErrorParams): APICallError {
const metadata: GrokCliErrorMetadata = {
code,
exitCode,
stderr,
stdout,
promptExcerpt
};
return new APICallError({
message,
isRetryable,
url: 'grok-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Create an authentication error
*/
export function createAuthenticationError({
message
}: CreateAuthenticationErrorParams): LoadAPIKeyError {
return new LoadAPIKeyError({
message:
message ||
'Authentication failed. Please ensure Grok CLI is properly configured with API key.'
});
}
/**
* Create a timeout error
*/
export function createTimeoutError({
message,
promptExcerpt,
timeoutMs
}: CreateTimeoutErrorParams): APICallError {
const metadata: GrokCliErrorMetadata & { timeoutMs: number } = {
code: 'TIMEOUT',
promptExcerpt,
timeoutMs
};
return new APICallError({
message,
isRetryable: true,
url: 'grok-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Create a CLI installation error
*/
export function createInstallationError({
message
}: CreateInstallationErrorParams): APICallError {
return new APICallError({
message:
message ||
'Grok CLI is not installed or not found in PATH. Please install with: npm install -g @vibe-kit/grok-cli',
isRetryable: false,
url: 'grok-cli://installation',
requestBodyValues: undefined
});
}
/**
* Check if an error is an authentication error
*/
export function isAuthenticationError(
error: unknown
): error is LoadAPIKeyError {
if (error instanceof LoadAPIKeyError) return true;
if (error instanceof APICallError) {
const metadata = error.data as GrokCliErrorMetadata | undefined;
if (!metadata) return false;
return (
metadata.exitCode === 401 ||
metadata.code === 'AUTHENTICATION_ERROR' ||
metadata.code === 'UNAUTHORIZED'
);
}
return false;
}
/**
* Check if an error is a timeout error
*/
export function isTimeoutError(error: unknown): error is APICallError {
if (
error instanceof APICallError &&
(error.data as GrokCliErrorMetadata)?.code === 'TIMEOUT'
)
return true;
return false;
}
/**
* Check if an error is an installation error
*/
export function isInstallationError(error: unknown): error is APICallError {
if (error instanceof APICallError && error.url === 'grok-cli://installation')
return true;
return false;
}
/**
* Get error metadata from an error
*/
export function getErrorMetadata(
error: unknown
): GrokCliErrorMetadata | undefined {
if (error instanceof APICallError && error.data) {
return error.data as GrokCliErrorMetadata;
}
return undefined;
}

View File

@@ -1,53 +1,51 @@
/**
* @fileoverview Grok CLI Language Model implementation
* Grok CLI Language Model implementation for AI SDK v5
*/
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import type {
LanguageModelV2,
LanguageModelV2CallOptions,
LanguageModelV2CallWarning
} from '@ai-sdk/provider';
import { NoSuchModelError } from '@ai-sdk/provider';
import { generateId } from '@ai-sdk/provider-utils';
import {
createPromptFromMessages,
convertFromGrokCliResponse,
escapeShellArg
} from './message-converter.js';
import { extractJson } from './json-extractor.js';
import {
createAPICallError,
createAuthenticationError,
createInstallationError,
createTimeoutError
} from './errors.js';
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { extractJson } from './json-extractor.js';
import {
convertFromGrokCliResponse,
createPromptFromMessages,
escapeShellArg
} from './message-converter.js';
import type {
GrokCliLanguageModelOptions,
GrokCliModelId,
GrokCliSettings
} from './types.js';
/**
* @typedef {import('./types.js').GrokCliSettings} GrokCliSettings
* @typedef {import('./types.js').GrokCliModelId} GrokCliModelId
* Grok CLI Language Model implementation for AI SDK v5
*/
export class GrokCliLanguageModel implements LanguageModelV2 {
readonly specificationVersion = 'v2' as const;
readonly defaultObjectGenerationMode = 'json' as const;
readonly supportsImageUrls = false;
readonly supportsStructuredOutputs = false;
readonly supportedUrls: Record<string, RegExp[]> = {};
/**
* @typedef {Object} GrokCliLanguageModelOptions
* @property {GrokCliModelId} id - Model ID
* @property {GrokCliSettings} [settings] - Model settings
*/
readonly modelId: GrokCliModelId;
readonly settings: GrokCliSettings;
export class GrokCliLanguageModel {
specificationVersion = 'v1';
defaultObjectGenerationMode = 'json';
supportsImageUrls = false;
supportsStructuredOutputs = false;
/** @type {GrokCliModelId} */
modelId;
/** @type {GrokCliSettings} */
settings;
/**
* @param {GrokCliLanguageModelOptions} options
*/
constructor(options) {
constructor(options: GrokCliLanguageModelOptions) {
this.modelId = options.id;
this.settings = options.settings ?? {};
@@ -64,15 +62,14 @@ export class GrokCliLanguageModel {
}
}
get provider() {
get provider(): string {
return 'grok-cli';
}
/**
* Check if Grok CLI is installed and available
* @returns {Promise<boolean>}
*/
async checkGrokCliInstallation() {
private async checkGrokCliInstallation(): Promise<boolean> {
return new Promise((resolve) => {
const child = spawn('grok', ['--version'], {
stdio: 'pipe'
@@ -85,9 +82,8 @@ export class GrokCliLanguageModel {
/**
* Get API key from settings or environment
* @returns {Promise<string|null>}
*/
async getApiKey() {
private async getApiKey(): Promise<string | null> {
// Check settings first
if (this.settings.apiKey) {
return this.settings.apiKey;
@@ -111,22 +107,32 @@ export class GrokCliLanguageModel {
/**
* Execute Grok CLI command
* @param {Array<string>} args - Command line arguments
* @param {Object} options - Execution options
* @returns {Promise<{stdout: string, stderr: string, exitCode: number}>}
*/
async executeGrokCli(args, options = {}) {
const timeout = options.timeout || this.settings.timeout || 120000; // 2 minutes default
private async executeGrokCli(
args: string[],
options: { timeout?: number; apiKey?: string } = {}
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
// Default timeout based on model type
let defaultTimeout = 120000; // 2 minutes default
if (this.modelId.includes('grok-4')) {
defaultTimeout = 600000; // 10 minutes for grok-4 models (they seem to hang during setup)
}
const timeout = options.timeout ?? this.settings.timeout ?? defaultTimeout;
return new Promise((resolve, reject) => {
const child = spawn('grok', args, {
stdio: 'pipe',
cwd: this.settings.workingDirectory || process.cwd()
cwd: this.settings.workingDirectory || process.cwd(),
env:
options.apiKey === undefined
? process.env
: { ...process.env, GROK_CLI_API_KEY: options.apiKey }
});
let stdout = '';
let stderr = '';
let timeoutId;
let timeoutId: NodeJS.Timeout | undefined;
// Set up timeout
if (timeout > 0) {
@@ -142,24 +148,26 @@ export class GrokCliLanguageModel {
}, timeout);
}
child.stdout.on('data', (data) => {
stdout += data.toString();
child.stdout?.on('data', (data) => {
const chunk = data.toString();
stdout += chunk;
});
child.stderr.on('data', (data) => {
stderr += data.toString();
child.stderr?.on('data', (data) => {
const chunk = data.toString();
stderr += chunk;
});
child.on('error', (error) => {
if (timeoutId) clearTimeout(timeoutId);
if (error.code === 'ENOENT') {
if ((error as any).code === 'ENOENT') {
reject(createInstallationError({}));
} else {
reject(
createAPICallError({
message: `Failed to execute Grok CLI: ${error.message}`,
code: error.code,
code: (error as any).code,
stderr: error.message,
isRetryable: false
})
@@ -180,15 +188,18 @@ export class GrokCliLanguageModel {
}
/**
* Generate unsupported parameter warnings
* @param {Object} options - Generation options
* @returns {Array} Warnings array
* Generate comprehensive warnings for unsupported parameters and validation issues
*/
generateUnsupportedWarnings(options) {
const warnings = [];
const unsupportedParams = [];
private generateAllWarnings(
options: LanguageModelV2CallOptions,
prompt: string
): LanguageModelV2CallWarning[] {
const warnings: LanguageModelV2CallWarning[] = [];
const unsupportedParams: string[] = [];
// Grok CLI supports some parameters but not all AI SDK parameters
// Check for unsupported parameters
if (options.temperature !== undefined)
unsupportedParams.push('temperature');
if (options.topP !== undefined) unsupportedParams.push('topP');
if (options.topK !== undefined) unsupportedParams.push('topK');
if (options.presencePenalty !== undefined)
@@ -200,24 +211,51 @@ export class GrokCliLanguageModel {
if (options.seed !== undefined) unsupportedParams.push('seed');
if (unsupportedParams.length > 0) {
// Add a warning for each unsupported parameter
for (const param of unsupportedParams) {
warnings.push({
type: 'unsupported-setting',
setting: param,
setting: param as
| 'temperature'
| 'topP'
| 'topK'
| 'presencePenalty'
| 'frequencyPenalty'
| 'stopSequences'
| 'seed',
details: `Grok CLI does not support the ${param} parameter. It will be ignored.`
});
}
}
// Add model validation warnings if needed
if (!this.modelId || this.modelId.trim() === '') {
warnings.push({
type: 'other',
message: 'Model ID is empty or invalid'
});
}
// Add prompt validation
if (!prompt || prompt.trim() === '') {
warnings.push({
type: 'other',
message: 'Prompt is empty'
});
}
return warnings;
}
/**
* Generate text using Grok CLI
* @param {Object} options - Generation options
* @returns {Promise<Object>}
*/
async doGenerate(options) {
async doGenerate(options: LanguageModelV2CallOptions) {
// Handle abort signal early
if (options.abortSignal?.aborted) {
throw options.abortSignal.reason || new Error('Request aborted');
}
// Check CLI installation
const isInstalled = await this.checkGrokCliInstallation();
if (!isInstalled) {
@@ -234,7 +272,7 @@ export class GrokCliLanguageModel {
}
const prompt = createPromptFromMessages(options.prompt);
const warnings = this.generateUnsupportedWarnings(options);
const warnings = this.generateAllWarnings(options, prompt);
// Build command arguments
const args = ['--prompt', escapeShellArg(prompt)];
@@ -244,10 +282,11 @@ export class GrokCliLanguageModel {
args.push('--model', this.modelId);
}
// Add API key if available
if (apiKey) {
args.push('--api-key', apiKey);
}
// Skip API key parameter if it's likely already configured to avoid hanging
// The CLI seems to hang when trying to save API keys for grok-4 models
// if (apiKey) {
// args.push('--api-key', apiKey);
// }
// Add base URL if provided in settings
if (this.settings.baseURL) {
@@ -260,9 +299,7 @@ export class GrokCliLanguageModel {
}
try {
const result = await this.executeGrokCli(args, {
timeout: this.settings.timeout
});
const result = await this.executeGrokCli(args, { apiKey });
if (result.exitCode !== 0) {
// Handle authentication errors
@@ -290,19 +327,37 @@ export class GrokCliLanguageModel {
let text = response.text || '';
// Extract JSON if in object-json mode
if (options.mode?.type === 'object-json' && text) {
const isObjectJson = (
o: unknown
): o is { mode: { type: 'object-json' } } =>
!!o &&
typeof o === 'object' &&
'mode' in o &&
(o as any).mode?.type === 'object-json';
if (isObjectJson(options) && text) {
text = extractJson(text);
}
return {
text: text || undefined,
usage: response.usage || { promptTokens: 0, completionTokens: 0 },
finishReason: 'stop',
content: [
{
type: 'text' as const,
text: text || ''
}
],
usage: response.usage
? {
inputTokens: response.usage.promptTokens,
outputTokens: response.usage.completionTokens,
totalTokens: response.usage.totalTokens
}
: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
finishReason: 'stop' as const,
rawCall: {
rawPrompt: prompt,
rawSettings: args
},
warnings: warnings.length > 0 ? warnings : undefined,
warnings: warnings,
response: {
id: generateId(),
timestamp: new Date(),
@@ -314,20 +369,23 @@ export class GrokCliLanguageModel {
providerMetadata: {
'grok-cli': {
exitCode: result.exitCode,
stderr: result.stderr || undefined
...(result.stderr && { stderr: result.stderr })
}
}
};
} catch (error) {
// Re-throw our custom errors
if (error.name === 'APICallError' || error.name === 'LoadAPIKeyError') {
if (
(error as any).name === 'APICallError' ||
(error as any).name === 'LoadAPIKeyError'
) {
throw error;
}
// Wrap other errors
throw createAPICallError({
message: `Grok CLI execution failed: ${error.message}`,
code: error.code,
message: `Grok CLI execution failed: ${(error as Error).message}`,
code: (error as any).code,
promptExcerpt: prompt.substring(0, 200),
isRetryable: false
});
@@ -338,15 +396,39 @@ export class GrokCliLanguageModel {
* Stream text using Grok CLI
* Note: Grok CLI doesn't natively support streaming, so this simulates streaming
* by generating the full response and then streaming it in chunks
* @param {Object} options - Stream options
* @returns {Promise<Object>}
*/
async doStream(options) {
const warnings = this.generateUnsupportedWarnings(options);
async doStream(options: LanguageModelV2CallOptions) {
const prompt = createPromptFromMessages(options.prompt);
const warnings = this.generateAllWarnings(options, prompt);
const stream = new ReadableStream({
start: async (controller) => {
let abortListener: (() => void) | undefined;
try {
// Handle abort signal
if (options.abortSignal?.aborted) {
throw options.abortSignal.reason || new Error('Request aborted');
}
// Set up abort listener
if (options.abortSignal) {
abortListener = () => {
controller.enqueue({
type: 'error',
error:
options.abortSignal?.reason || new Error('Request aborted')
});
controller.close();
};
options.abortSignal.addEventListener('abort', abortListener, {
once: true
});
}
// Emit stream-start with warnings
controller.enqueue({ type: 'stream-start', warnings });
// Generate the full response first
const result = await this.doGenerate(options);
@@ -359,20 +441,48 @@ export class GrokCliLanguageModel {
});
// Simulate streaming by chunking the text
const text = result.text || '';
const content = result.content || [];
const text =
content.length > 0 && content[0].type === 'text'
? content[0].text
: '';
const chunkSize = 50; // Characters per chunk
let textPartId: string | undefined;
// Emit text-start if we have content
if (text.length > 0) {
textPartId = generateId();
controller.enqueue({
type: 'text-start',
id: textPartId
});
}
for (let i = 0; i < text.length; i += chunkSize) {
// Check for abort during streaming
if (options.abortSignal?.aborted) {
throw options.abortSignal.reason || new Error('Request aborted');
}
const chunk = text.slice(i, i + chunkSize);
controller.enqueue({
type: 'text-delta',
textDelta: chunk
id: textPartId!,
delta: chunk
});
// Add small delay to simulate streaming
await new Promise((resolve) => setTimeout(resolve, 20));
}
// Close text part if opened
if (textPartId) {
controller.enqueue({
type: 'text-end',
id: textPartId
});
}
// Emit finish event
controller.enqueue({
type: 'finish',
@@ -388,19 +498,22 @@ export class GrokCliLanguageModel {
error
});
controller.close();
} finally {
// Clean up abort listener
if (options.abortSignal && abortListener) {
options.abortSignal.removeEventListener('abort', abortListener);
}
}
},
cancel: () => {
// Clean up if stream is cancelled
}
});
return {
stream,
rawCall: {
rawPrompt: createPromptFromMessages(options.prompt),
rawSettings: {}
},
warnings: warnings.length > 0 ? warnings : undefined,
request: {
body: createPromptFromMessages(options.prompt)
body: prompt
}
};
}

View File

@@ -0,0 +1,121 @@
/**
* Tests for Grok CLI provider
*/
import { NoSuchModelError } from '@ai-sdk/provider';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { GrokCliLanguageModel } from './grok-cli-language-model.js';
import { createGrokCli, grokCli } from './grok-cli-provider.js';
// Mock the GrokCliLanguageModel
vi.mock('./grok-cli-language-model.js', () => ({
GrokCliLanguageModel: vi.fn().mockImplementation((options) => ({
modelId: options.id,
settings: options.settings,
provider: 'grok-cli'
}))
}));
describe('createGrokCli', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should create a provider with default settings', () => {
const provider = createGrokCli();
expect(typeof provider).toBe('function');
expect(typeof provider.languageModel).toBe('function');
expect(typeof provider.chat).toBe('function');
expect(typeof provider.textEmbeddingModel).toBe('function');
expect(typeof provider.imageModel).toBe('function');
});
it('should create a provider with custom default settings', () => {
const defaultSettings = {
timeout: 5000,
workingDirectory: '/custom/path'
};
const provider = createGrokCli({ defaultSettings });
const model = provider('grok-2-mini');
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
id: 'grok-2-mini',
settings: defaultSettings
});
});
it('should create language models with merged settings', () => {
const defaultSettings = { timeout: 5000 };
const provider = createGrokCli({ defaultSettings });
const modelSettings = { apiKey: 'test-key' };
const model = provider('grok-2', modelSettings);
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
id: 'grok-2',
settings: { timeout: 5000, apiKey: 'test-key' }
});
});
it('should create models via languageModel method', () => {
const provider = createGrokCli();
const model = provider.languageModel('grok-2-mini', { timeout: 1000 });
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
id: 'grok-2-mini',
settings: { timeout: 1000 }
});
});
it('should create models via chat method (alias)', () => {
const provider = createGrokCli();
const model = provider.chat('grok-2');
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
id: 'grok-2',
settings: {}
});
});
it('should throw error when called with new keyword', () => {
const provider = createGrokCli();
expect(() => {
// @ts-expect-error - intentionally testing invalid usage
new provider('grok-2');
}).toThrow(
'The Grok CLI model function cannot be called with the new keyword.'
);
});
it('should throw NoSuchModelError for textEmbeddingModel', () => {
const provider = createGrokCli();
expect(() => {
provider.textEmbeddingModel('test-model');
}).toThrow(NoSuchModelError);
});
it('should throw NoSuchModelError for imageModel', () => {
const provider = createGrokCli();
expect(() => {
provider.imageModel('test-model');
}).toThrow(NoSuchModelError);
});
});
describe('default grokCli provider', () => {
it('should be a pre-configured provider instance', () => {
expect(typeof grokCli).toBe('function');
expect(typeof grokCli.languageModel).toBe('function');
expect(typeof grokCli.chat).toBe('function');
});
it('should create models with default configuration', () => {
const model = grokCli('grok-2-mini');
expect(GrokCliLanguageModel).toHaveBeenCalledWith({
id: 'grok-2-mini',
settings: {}
});
});
});

View File

@@ -0,0 +1,108 @@
/**
* Grok CLI provider implementation for AI SDK v5
*/
import type { LanguageModelV2, ProviderV2 } from '@ai-sdk/provider';
import { NoSuchModelError } from '@ai-sdk/provider';
import { GrokCliLanguageModel } from './grok-cli-language-model.js';
import type { GrokCliModelId, GrokCliSettings } from './types.js';
/**
* Grok CLI provider interface that extends the AI SDK's ProviderV2
*/
export interface GrokCliProvider extends ProviderV2 {
/**
* Creates a language model instance for the specified model ID.
* This is a shorthand for calling `languageModel()`.
*/
(modelId: GrokCliModelId, settings?: GrokCliSettings): LanguageModelV2;
/**
* Creates a language model instance for text generation.
*/
languageModel(
modelId: GrokCliModelId,
settings?: GrokCliSettings
): LanguageModelV2;
/**
* Alias for `languageModel()` to maintain compatibility with AI SDK patterns.
*/
chat(modelId: GrokCliModelId, settings?: GrokCliSettings): LanguageModelV2;
textEmbeddingModel(modelId: string): never;
imageModel(modelId: string): never;
}
/**
* Configuration options for creating a Grok CLI provider instance
*/
export interface GrokCliProviderSettings {
/**
* Default settings to use for all models created by this provider.
* Individual model settings will override these defaults.
*/
defaultSettings?: GrokCliSettings;
}
/**
* Creates a Grok CLI provider instance with the specified configuration.
* The provider can be used to create language models for interacting with Grok models.
*/
export function createGrokCli(
options: GrokCliProviderSettings = {}
): GrokCliProvider {
const createModel = (
modelId: GrokCliModelId,
settings: GrokCliSettings = {}
): LanguageModelV2 => {
const mergedSettings = {
...options.defaultSettings,
...settings
};
return new GrokCliLanguageModel({
id: modelId,
settings: mergedSettings
});
};
const provider = function (
modelId: GrokCliModelId,
settings?: GrokCliSettings
) {
if (new.target) {
throw new Error(
'The Grok CLI model function cannot be called with the new keyword.'
);
}
return createModel(modelId, settings);
};
provider.languageModel = createModel;
provider.chat = createModel; // Alias for languageModel
// Add textEmbeddingModel method that throws NoSuchModelError
provider.textEmbeddingModel = (modelId: string) => {
throw new NoSuchModelError({
modelId,
modelType: 'textEmbeddingModel'
});
};
provider.imageModel = (modelId: string) => {
throw new NoSuchModelError({
modelId,
modelType: 'imageModel'
});
};
return provider as GrokCliProvider;
}
/**
* Default Grok CLI provider instance.
* Pre-configured provider for quick usage without custom settings.
*/
export const grokCli = createGrokCli();

View File

@@ -0,0 +1,64 @@
/**
* Provider exports for creating and configuring Grok CLI instances.
*/
/**
* Creates a new Grok CLI provider instance and the default provider instance.
*/
export { createGrokCli, grokCli } from './grok-cli-provider.js';
/**
* Type definitions for the Grok CLI provider.
*/
export type {
GrokCliProvider,
GrokCliProviderSettings
} from './grok-cli-provider.js';
/**
* Language model implementation for Grok CLI.
* This class implements the AI SDK's LanguageModelV2 interface.
*/
export { GrokCliLanguageModel } from './grok-cli-language-model.js';
/**
* Type definitions for Grok CLI language models.
*/
export type {
GrokCliModelId,
GrokCliLanguageModelOptions,
GrokCliSettings,
GrokCliMessage,
GrokCliResponse,
GrokCliErrorMetadata
} from './types.js';
/**
* Error handling utilities for Grok CLI.
* These functions help create and identify specific error types.
*/
export {
isAuthenticationError,
isTimeoutError,
isInstallationError,
getErrorMetadata,
createAPICallError,
createAuthenticationError,
createTimeoutError,
createInstallationError
} from './errors.js';
/**
* Message conversion utilities for Grok CLI communication.
*/
export {
convertToGrokCliMessages,
convertFromGrokCliResponse,
createPromptFromMessages,
escapeShellArg
} from './message-converter.js';
/**
* JSON extraction utilities for parsing Grok responses.
*/
export { extractJson } from './json-extractor.js';

View File

@@ -0,0 +1,81 @@
/**
* Tests for JSON extraction utilities
*/
import { describe, expect, it } from 'vitest';
import { extractJson } from './json-extractor.js';
describe('extractJson', () => {
it('should extract JSON from markdown code blocks', () => {
const text = '```json\n{"name": "test", "value": 42}\n```';
const result = extractJson(text);
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should extract JSON from generic code blocks', () => {
const text = '```\n{"name": "test", "value": 42}\n```';
const result = extractJson(text);
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should remove JavaScript variable declarations', () => {
const text = 'const result = {"name": "test", "value": 42};';
const result = extractJson(text);
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should handle let variable declarations', () => {
const text = 'let data = {"name": "test", "value": 42};';
const result = extractJson(text);
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should handle var variable declarations', () => {
const text = 'var config = {"name": "test", "value": 42};';
const result = extractJson(text);
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should extract JSON arrays', () => {
const text = '[{"name": "test1"}, {"name": "test2"}]';
const result = extractJson(text);
expect(JSON.parse(result)).toEqual([{ name: 'test1' }, { name: 'test2' }]);
});
it('should convert JavaScript object literals to JSON', () => {
const text = "{name: 'test', value: 42}";
const result = extractJson(text);
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should return valid JSON (canonical formatting)', () => {
const text = '{"name": "test", "value": 42}';
const result = extractJson(text);
expect(JSON.parse(result)).toEqual({ name: 'test', value: 42 });
});
it('should return original text when JSON parsing fails completely', () => {
const text = 'This is not JSON at all';
const result = extractJson(text);
expect(result).toBe('This is not JSON at all');
});
it('should handle complex nested objects', () => {
const text =
'```json\n{\n "user": {\n "name": "John",\n "age": 30\n },\n "items": [1, 2, 3]\n}\n```';
const result = extractJson(text);
expect(JSON.parse(result)).toEqual({
user: {
name: 'John',
age: 30
},
items: [1, 2, 3]
});
});
it('should handle mixed quotes in object literals', () => {
const text = `{name: "test", value: 'mixed quotes'}`;
const result = extractJson(text);
expect(JSON.parse(result)).toEqual({ name: 'test', value: 'mixed quotes' });
});
});

View File

@@ -0,0 +1,132 @@
/**
* Extract JSON from AI's response using a tolerant parser.
*
* The function removes common wrappers such as markdown fences or variable
* declarations and then attempts to parse the remaining text with
* `jsonc-parser`. If valid JSON (or JSONC) can be parsed, it is returned as a
* string via `JSON.stringify`. Otherwise the original text is returned.
*
* @param text - Raw text which may contain JSON
* @returns A valid JSON string if extraction succeeds, otherwise the original text
*/
import { parse, type ParseError } from 'jsonc-parser';
export function extractJson(text: string): string {
let content = text.trim();
// Strip ```json or ``` fences
const fenceMatch = /```(?:json)?\s*([\s\S]*?)\s*```/i.exec(content);
if (fenceMatch) {
content = fenceMatch[1];
}
// Strip variable declarations like `const foo =` or `let foo =`
const varMatch = /^\s*(?:const|let|var)\s+\w+\s*=\s*([\s\S]*)/i.exec(content);
if (varMatch) {
content = varMatch[1];
// Remove trailing semicolon if present
if (content.trim().endsWith(';')) {
content = content.trim().slice(0, -1);
}
}
// Find the first opening bracket
const firstObj = content.indexOf('{');
const firstArr = content.indexOf('[');
if (firstObj === -1 && firstArr === -1) {
return text;
}
const start =
firstArr === -1
? firstObj
: firstObj === -1
? firstArr
: Math.min(firstObj, firstArr);
content = content.slice(start);
// Try to parse the entire string with jsonc-parser
const tryParse = (value: string): string | undefined => {
const errors: ParseError[] = [];
try {
const result = parse(value, errors, { allowTrailingComma: true });
if (errors.length === 0) {
return JSON.stringify(result, null, 2);
}
} catch {
// ignore
}
return undefined;
};
const parsed = tryParse(content);
if (parsed !== undefined) {
return parsed;
}
// If parsing the full string failed, use a more efficient approach
// to find valid JSON boundaries
const openChar = content[0];
const closeChar = openChar === '{' ? '}' : ']';
// Find all potential closing positions by tracking nesting depth
const closingPositions: number[] = [];
let depth = 0;
let inString = false;
let escapeNext = false;
for (let i = 0; i < content.length; i++) {
const char = content[i];
if (escapeNext) {
escapeNext = false;
continue;
}
if (char === '\\') {
escapeNext = true;
continue;
}
if (char === '"' && !inString) {
inString = true;
continue;
}
if (char === '"' && inString) {
inString = false;
continue;
}
// Skip content inside strings
if (inString) continue;
if (char === openChar) {
depth++;
} else if (char === closeChar) {
depth--;
if (depth === 0) {
closingPositions.push(i + 1);
}
}
}
// Try parsing at each valid closing position, starting from the end
for (let i = closingPositions.length - 1; i >= 0; i--) {
const attempt = tryParse(content.slice(0, closingPositions[i]));
if (attempt !== undefined) {
return attempt;
}
}
// As a final fallback, try the original character-by-character approach
// but only for the last 1000 characters to limit performance impact
const searchStart = Math.max(0, content.length - 1000);
for (let end = content.length - 1; end > searchStart; end--) {
const attempt = tryParse(content.slice(0, end));
if (attempt !== undefined) {
return attempt;
}
}
return text;
}

View File

@@ -0,0 +1,163 @@
/**
* Tests for message conversion utilities
*/
import { describe, expect, it } from 'vitest';
import {
convertFromGrokCliResponse,
convertToGrokCliMessages,
createPromptFromMessages,
escapeShellArg
} from './message-converter.js';
describe('convertToGrokCliMessages', () => {
it('should convert string content messages', () => {
const messages = [
{ role: 'user', content: 'Hello, world!' },
{ role: 'assistant', content: 'Hi there!' }
];
const result = convertToGrokCliMessages(messages);
expect(result).toEqual([
{ role: 'user', content: 'Hello, world!' },
{ role: 'assistant', content: 'Hi there!' }
]);
});
it('should convert array content messages', () => {
const messages = [
{
role: 'user',
content: [
{ type: 'text', text: 'Hello' },
{ type: 'text', text: 'World' }
]
}
];
const result = convertToGrokCliMessages(messages);
expect(result).toEqual([{ role: 'user', content: 'Hello\nWorld' }]);
});
it('should convert object content messages', () => {
const messages = [
{
role: 'user',
content: { text: 'Hello from object' }
}
];
const result = convertToGrokCliMessages(messages);
expect(result).toEqual([{ role: 'user', content: 'Hello from object' }]);
});
});
describe('convertFromGrokCliResponse', () => {
it('should parse JSONL response format', () => {
const responseText = `{"role": "assistant", "content": "Hello there!", "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}`;
const result = convertFromGrokCliResponse(responseText);
expect(result).toEqual({
text: 'Hello there!',
usage: {
promptTokens: 10,
completionTokens: 5,
totalTokens: 15
}
});
});
it('should handle multiple lines in JSONL format', () => {
const responseText = `{"role": "user", "content": "Hello"}
{"role": "assistant", "content": "Hi there!", "usage": {"prompt_tokens": 5, "completion_tokens": 3}}`;
const result = convertFromGrokCliResponse(responseText);
expect(result).toEqual({
text: 'Hi there!',
usage: {
promptTokens: 5,
completionTokens: 3,
totalTokens: 0
}
});
});
it('should fallback to raw text when parsing fails', () => {
const responseText = 'Invalid JSON response';
const result = convertFromGrokCliResponse(responseText);
expect(result).toEqual({
text: 'Invalid JSON response',
usage: undefined
});
});
});
describe('createPromptFromMessages', () => {
it('should create formatted prompt from messages', () => {
const messages = [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'What is 2+2?' },
{ role: 'assistant', content: '2+2 equals 4.' }
];
const result = createPromptFromMessages(messages);
expect(result).toBe(
'System: You are a helpful assistant.\n\nUser: What is 2+2?\n\nAssistant: 2+2 equals 4.'
);
});
it('should handle custom role names', () => {
const messages = [{ role: 'custom', content: 'Custom message' }];
const result = createPromptFromMessages(messages);
expect(result).toBe('custom: Custom message');
});
it('should trim whitespace from message content', () => {
const messages = [
{ role: 'user', content: ' Hello with spaces ' },
{ role: 'assistant', content: '\n\nResponse with newlines\n\n' }
];
const result = createPromptFromMessages(messages);
expect(result).toBe(
'User: Hello with spaces\n\nAssistant: Response with newlines'
);
});
});
describe('escapeShellArg', () => {
it('should escape single quotes', () => {
const arg = "It's a test";
const result = escapeShellArg(arg);
expect(result).toBe("'It'\\''s a test'");
});
it('should handle strings without special characters', () => {
const arg = 'simple string';
const result = escapeShellArg(arg);
expect(result).toBe("'simple string'");
});
it('should convert non-string values to strings', () => {
const arg = 123;
const result = escapeShellArg(arg);
expect(result).toBe("'123'");
});
it('should handle empty strings', () => {
const arg = '';
const result = escapeShellArg(arg);
expect(result).toBe("''");
});
});

View File

@@ -1,17 +1,28 @@
/**
* @fileoverview Message format conversion utilities for Grok CLI provider
* Message format conversion utilities for Grok CLI provider
*/
import type { GrokCliMessage, GrokCliResponse } from './types.js';
/**
* @typedef {import('./types.js').GrokCliMessage} GrokCliMessage
* AI SDK message type (simplified interface)
*/
interface AISDKMessage {
role: string;
content:
| string
| Array<{ type: string; text?: string }>
| { text?: string; [key: string]: unknown };
}
/**
* Convert AI SDK messages to Grok CLI compatible format
* @param {Array<Object>} messages - AI SDK message array
* @returns {Array<GrokCliMessage>} Grok CLI compatible messages
* @param messages - AI SDK message array
* @returns Grok CLI compatible messages
*/
export function convertToGrokCliMessages(messages) {
export function convertToGrokCliMessages(
messages: AISDKMessage[]
): GrokCliMessage[] {
return messages.map((message) => {
// Handle different message content types
let content = '';
@@ -22,7 +33,7 @@ export function convertToGrokCliMessages(messages) {
// Handle multi-part content (text and images)
content = message.content
.filter((part) => part.type === 'text')
.map((part) => part.text)
.map((part) => part.text || '')
.join('\n');
} else if (message.content && typeof message.content === 'object') {
// Handle object content
@@ -38,10 +49,17 @@ export function convertToGrokCliMessages(messages) {
/**
* Convert Grok CLI response to AI SDK format
* @param {string} responseText - Raw response text from Grok CLI (JSONL format)
* @returns {Object} AI SDK compatible response object
* @param responseText - Raw response text from Grok CLI (JSONL format)
* @returns AI SDK compatible response object
*/
export function convertFromGrokCliResponse(responseText) {
export function convertFromGrokCliResponse(responseText: string): {
text: string;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
} {
try {
// Grok CLI outputs JSONL format - each line is a separate JSON message
const lines = responseText
@@ -50,10 +68,10 @@ export function convertFromGrokCliResponse(responseText) {
.filter((line) => line.trim());
// Parse each line as JSON and find assistant messages
const messages = [];
const messages: GrokCliResponse[] = [];
for (const line of lines) {
try {
const message = JSON.parse(line);
const message = JSON.parse(line) as GrokCliResponse;
messages.push(message);
} catch (parseError) {
// Skip invalid JSON lines
@@ -95,10 +113,10 @@ export function convertFromGrokCliResponse(responseText) {
/**
* Create a prompt string for Grok CLI from messages
* @param {Array<Object>} messages - AI SDK message array
* @returns {string} Formatted prompt string
* @param messages - AI SDK message array
* @returns Formatted prompt string
*/
export function createPromptFromMessages(messages) {
export function createPromptFromMessages(messages: AISDKMessage[]): string {
const grokMessages = convertToGrokCliMessages(messages);
// Create a conversation-style prompt
@@ -122,14 +140,14 @@ export function createPromptFromMessages(messages) {
/**
* Escape shell arguments for safe CLI execution
* @param {string} arg - Argument to escape
* @returns {string} Shell-escaped argument
* @param arg - Argument to escape
* @returns Shell-escaped argument
*/
export function escapeShellArg(arg) {
export function escapeShellArg(arg: string | unknown): string {
if (typeof arg !== 'string') {
arg = String(arg);
}
// Replace single quotes with '\''
return "'" + arg.replace(/'/g, "'\\''") + "'";
return "'" + (arg as string).replace(/'/g, "'\\''") + "'";
}

View File

@@ -0,0 +1,81 @@
/**
* Type definitions for Grok CLI provider
*/
/**
* Settings for configuring Grok CLI behavior
*/
export interface GrokCliSettings {
/** API key for Grok CLI */
apiKey?: string;
/** Base URL for Grok API */
baseURL?: string;
/** Default model to use */
model?: string;
/** Timeout in milliseconds */
timeout?: number;
/** Working directory for CLI commands */
workingDirectory?: string;
}
/**
* Model identifiers supported by Grok CLI
*/
export type GrokCliModelId = string;
/**
* Error metadata for Grok CLI operations
*/
export interface GrokCliErrorMetadata {
/** Error code */
code?: string;
/** Process exit code */
exitCode?: number;
/** Standard error output */
stderr?: string;
/** Standard output */
stdout?: string;
/** Excerpt of the prompt that caused the error */
promptExcerpt?: string;
/** Timeout value in milliseconds */
timeoutMs?: number;
}
/**
* Message format for Grok CLI communication
*/
export interface GrokCliMessage {
/** Message role (user, assistant, system) */
role: string;
/** Message content */
content: string;
}
/**
* Response format from Grok CLI
*/
export interface GrokCliResponse {
/** Message role */
role: string;
/** Response content */
content: string;
/** Token usage information */
usage?: {
/** Input tokens used */
prompt_tokens?: number;
/** Output tokens used */
completion_tokens?: number;
/** Total tokens used */
total_tokens?: number;
};
}
/**
* Configuration options for Grok CLI language model
*/
export interface GrokCliLanguageModelOptions {
/** Model identifier */
id: GrokCliModelId;
/** Model settings */
settings?: GrokCliSettings;
}

View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"baseUrl": ".",
"rootDir": "./src",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"moduleDetection": "force",
"types": ["node"],
"resolveJsonModule": true,
"isolatedModules": true,
"allowImportingTsExtensions": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
}

View File

@@ -20,8 +20,7 @@
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"dotenv-mono": "^1.5.1",
"typescript": "^5.7.3"
"typescript": "^5.9.2"
},
"dependencies": {
"tsup": "^8.5.0"

View File

@@ -43,9 +43,9 @@ export const baseConfig: Partial<UserConfig> = {
export function mergeConfig(
base: Partial<UserConfig>,
overrides: Partial<UserConfig>
): Partial<UserConfig> {
): UserConfig {
return {
...base,
...overrides
};
} as UserConfig;
}

View File

@@ -31,21 +31,13 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.57.4",
"zod": "^3.23.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@tm/build-config": "*",
"@types/node": "^22.10.5",
"@vitest/coverage-v8": "^2.0.5",
"dotenv-mono": "^1.5.1",
"ts-node": "^10.9.2",
"tsup": "^8.5.0",
"typescript": "^5.7.3",
"vitest": "^2.1.8"
},
"engines": {
"node": ">=18.0.0"
"@vitest/coverage-v8": "^3.2.4",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
},
"files": ["src", "README.md", "CHANGELOG.md"],
"keywords": ["task-management", "typescript", "ai", "prd", "parser"],

View File

@@ -135,15 +135,28 @@ export class TaskService {
}
/**
* Get a single task by ID
* Get a single task by ID - delegates to storage layer
*/
async getTask(taskId: string, tag?: string): Promise<Task | null> {
const result = await this.getTaskList({
tag,
includeSubtasks: true
});
// Use provided tag or get active tag
const activeTag = tag || this.getActiveTag();
return result.tasks.find((t) => t.id === taskId) || null;
try {
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
return await this.storage.loadTask(String(taskId), activeTag);
} catch (error) {
throw new TaskMasterError(
`Failed to get task ${taskId}`,
ERROR_CODES.STORAGE_ERROR,
{
operation: 'getTask',
resource: 'task',
taskId: String(taskId),
tag: activeTag
},
error as Error
);
}
}
/**

View File

@@ -105,9 +105,65 @@ export class FileStorage implements IStorage {
/**
* Load a single task by ID from the tasks.json file
* Handles both regular tasks and subtasks (with dotted notation like "1.2")
*/
async loadTask(taskId: string, tag?: string): Promise<Task | null> {
const tasks = await this.loadTasks(tag);
// Check if this is a subtask (contains a dot)
if (taskId.includes('.')) {
const [parentId, subtaskId] = taskId.split('.');
const parentTask = tasks.find((t) => String(t.id) === parentId);
if (!parentTask || !parentTask.subtasks) {
return null;
}
const subtask = parentTask.subtasks.find(
(st) => String(st.id) === subtaskId
);
if (!subtask) {
return null;
}
const toFullSubId = (maybeDotId: string | number): string => {
const depId = String(maybeDotId);
return depId.includes('.') ? depId : `${parentTask.id}.${depId}`;
};
const resolvedDependencies =
subtask.dependencies?.map((dep) => toFullSubId(dep)) ?? [];
// Return a Task-like object for the subtask with the full dotted ID
// Following the same pattern as findTaskById in utils.js
const subtaskResult = {
...subtask,
id: taskId, // Use the full dotted ID
title: subtask.title || `Subtask ${subtaskId}`,
description: subtask.description || '',
status: subtask.status || 'pending',
priority: subtask.priority || parentTask.priority || 'medium',
dependencies: resolvedDependencies,
details: subtask.details || '',
testStrategy: subtask.testStrategy || '',
subtasks: [],
tags: parentTask.tags || [],
assignee: subtask.assignee || parentTask.assignee,
complexity: subtask.complexity || parentTask.complexity,
createdAt: subtask.createdAt || parentTask.createdAt,
updatedAt: subtask.updatedAt || parentTask.updatedAt,
// Add reference to parent task for context (like utils.js does)
parentTask: {
id: parentTask.id,
title: parentTask.title,
status: parentTask.status
},
isSubtask: true
};
return subtaskResult;
}
// Handle regular task lookup
return tasks.find((task) => String(task.id) === String(taskId)) || null;
}
@@ -409,8 +465,11 @@ export class FileStorage implements IStorage {
const allDone = subs.every(isDoneLike);
const anyInProgress = subs.some((s) => norm(s) === 'in-progress');
const anyDone = subs.some(isDoneLike);
const allPending = subs.every((s) => norm(s) === 'pending');
if (allDone) parentNewStatus = 'done';
else if (anyInProgress || anyDone) parentNewStatus = 'in-progress';
else if (allPending) parentNewStatus = 'pending';
}
// Always bump updatedAt; update status only if changed

View File

@@ -82,7 +82,7 @@ export class StorageFactory {
apiAccessToken: credentials.token,
apiEndpoint:
config.storage?.apiEndpoint ||
process.env.HAMSTER_API_URL ||
process.env.TM_PUBLIC_BASE_DOMAIN ||
'https://tryhamster.com/api'
};
config.storage = nextStorage;
@@ -112,7 +112,7 @@ export class StorageFactory {
apiAccessToken: credentials.token,
apiEndpoint:
config.storage?.apiEndpoint ||
process.env.HAMSTER_API_URL ||
process.env.TM_PUBLIC_BASE_DOMAIN ||
'https://tryhamster.com/api'
};
config.storage = nextStorage;

View File

@@ -311,6 +311,7 @@ function validateClaudeCodeSettings(settings) {
// Define the base settings schema without commandSpecific first
const BaseSettingsSchema = z.object({
pathToClaudeCodeExecutable: z.string().optional(),
// Use number().int() for integer validation in Zod
maxTurns: z.number().int().positive().optional(),
customSystemPrompt: z.string().optional(),
appendSystemPrompt: z.string().optional(),
@@ -326,19 +327,22 @@ function validateClaudeCodeSettings(settings) {
type: z.enum(['stdio', 'sse']).optional(),
command: z.string(),
args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(),
url: z.string().url().optional(),
headers: z.record(z.string()).optional()
env: z.record(z.string(), z.string()).optional(),
url: z.url().optional(),
headers: z.record(z.string(), z.string()).optional()
})
)
.optional()
});
// Define CommandSpecificSchema using the base schema
const CommandSpecificSchema = z.record(
z.enum(AI_COMMAND_NAMES),
BaseSettingsSchema
);
// Define CommandSpecificSchema using flexible keys, but restrict to known commands
const CommandSpecificSchema = z
.record(z.string(), BaseSettingsSchema)
.refine(
(obj) =>
Object.keys(obj || {}).every((k) => AI_COMMAND_NAMES.includes(k)),
{ message: 'Invalid command name in commandSpecific' }
);
// Define the full settings schema with commandSpecific
const SettingsSchema = BaseSettingsSchema.extend({

View File

@@ -31,36 +31,30 @@ import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
// --- Zod Schemas (Keep from previous step) ---
const subtaskSchema = z
.object({
id: z
.number()
.int()
.positive()
.describe('Sequential subtask ID starting from 1'),
title: z.string().min(5).describe('Clear, specific title for the subtask'),
description: z
.string()
.min(10)
.describe('Detailed description of the subtask'),
dependencies: z
.array(z.string())
.describe(
'Array of subtask dependencies within the same parent task. Use format ["parentTaskId.1", "parentTaskId.2"]. Subtasks can only depend on siblings, not external tasks.'
),
details: z.string().min(20).describe('Implementation details and guidance'),
status: z
.string()
.describe(
'The current status of the subtask (should be pending initially)'
),
testStrategy: z
.string()
.nullable()
.describe('Approach for testing this subtask')
.default('')
})
.strict();
const subtaskSchema = z.strictObject({
id: z.int().positive().describe('Sequential subtask ID starting from 1'),
title: z.string().min(5).describe('Clear, specific title for the subtask'),
description: z
.string()
.min(10)
.describe('Detailed description of the subtask'),
dependencies: z
.array(z.string())
.describe(
'Array of subtask dependencies within the same parent task. Use format ["parentTaskId.1", "parentTaskId.2"]. Subtasks can only depend on siblings, not external tasks.'
),
details: z.string().min(20).describe('Implementation details and guidance'),
status: z
.string()
.describe(
'The current status of the subtask (should be pending initially)'
),
testStrategy: z
.string()
.nullable()
.describe('Approach for testing this subtask')
.prefault('')
});
const subtaskArraySchema = z.array(subtaskSchema);
const subtaskWrapperSchema = z.object({
subtasks: subtaskArraySchema.describe('The array of generated subtasks.')

View File

@@ -355,7 +355,7 @@ Ensure the JSON is valid and properly formatted.`;
const subtaskSchema = z.object({
subtasks: z.array(
z.object({
id: z.number().int().positive(),
id: z.int().positive(),
title: z.string().min(5),
description: z.string().min(10),
dependencies: z.array(z.string()),
@@ -386,14 +386,44 @@ Ensure the JSON is valid and properly formatted.`;
testStrategy: subtask.testStrategy || ''
}));
// Ensure new subtasks have unique sequential IDs after the preserved ones
const maxPreservedId = preservedSubtasks.reduce(
(max, st) => Math.max(max, st.id || 0),
0
);
let nextId = maxPreservedId + 1;
const idMapping = new Map();
const normalizedGeneratedSubtasks = processedGeneratedSubtasks
.map((st) => {
const originalId = st.id;
const newId = nextId++;
idMapping.set(originalId, newId);
return {
...st,
id: newId
};
})
.map((st) => ({
...st,
dependencies: (st.dependencies || []).map((dep) => {
if (typeof dep !== 'string' || !dep.startsWith(`${task.id}.`)) {
return dep;
}
const [, siblingIdPart] = dep.split('.');
const originalSiblingId = Number.parseInt(siblingIdPart, 10);
const remappedSiblingId = idMapping.get(originalSiblingId);
return remappedSiblingId ? `${task.id}.${remappedSiblingId}` : dep;
})
}));
// Update task with preserved subtasks + newly generated ones
task.subtasks = [...preservedSubtasks, ...processedGeneratedSubtasks];
task.subtasks = [...preservedSubtasks, ...normalizedGeneratedSubtasks];
return {
updatedTask: task,
regenerated: true,
preserved: preservedSubtasks.length,
generated: processedGeneratedSubtasks.length
generated: normalizedGeneratedSubtasks.length
};
} catch (error) {
log(

View File

@@ -619,9 +619,29 @@ async function tags(
headers.push(chalk.cyan.bold('Description'));
}
// Calculate dynamic column widths based on terminal width
const terminalWidth = Math.max(process.stdout.columns || 120, 80);
const usableWidth = Math.floor(terminalWidth * 0.95);
let colWidths;
if (showMetadata) {
// With metadata: Tag Name, Tasks, Completed, Created, Description
const widths = [0.25, 0.1, 0.12, 0.15, 0.38];
colWidths = widths.map((w, i) =>
Math.max(Math.floor(usableWidth * w), i === 0 ? 15 : 8)
);
} else {
// Without metadata: Tag Name, Tasks, Completed
const widths = [0.7, 0.15, 0.15];
colWidths = widths.map((w, i) =>
Math.max(Math.floor(usableWidth * w), i === 0 ? 20 : 10)
);
}
const table = new Table({
head: headers,
colWidths: showMetadata ? [20, 10, 12, 15, 50] : [25, 10, 12]
colWidths: colWidths,
wordWrap: true
});
// Add rows

View File

@@ -40,9 +40,9 @@ const updatedTaskSchema = z
description: z.string(),
status: z.string(),
dependencies: z.array(z.union([z.number().int(), z.string()])),
priority: z.string().nullable().default('medium'),
details: z.string().nullable().default(''),
testStrategy: z.string().nullable().default(''),
priority: z.string().nullable().prefault('medium'),
details: z.string().nullable().prefault(''),
testStrategy: z.string().nullable().prefault(''),
subtasks: z
.array(
z.object({
@@ -54,15 +54,15 @@ const updatedTaskSchema = z
title: z.string(),
description: z.string(),
status: z.string(),
dependencies: z.array(z.number().int()).nullable().default([]),
details: z.string().nullable().default(''),
testStrategy: z.string().nullable().default('')
dependencies: z.array(z.number().int()).nullable().prefault([]),
details: z.string().nullable().prefault(''),
testStrategy: z.string().nullable().prefault('')
})
)
.nullable()
.default([])
.prefault([])
})
.strip(); // Allows parsing even if AI adds extra fields, but validation focuses on schema
.strip(); // Enforce the canonical task shape and drop unknown fields
/**
* Parses a single updated task object from AI's text response.

View File

@@ -31,17 +31,17 @@ import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
// Zod schema for validating the structure of tasks AFTER parsing
const updatedTaskSchema = z
.object({
id: z.number().int(),
id: z.int(),
title: z.string(),
description: z.string(),
status: z.string(),
dependencies: z.array(z.union([z.number().int(), z.string()])),
dependencies: z.array(z.union([z.int(), z.string()])),
priority: z.string().nullable(),
details: z.string().nullable(),
testStrategy: z.string().nullable(),
subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now
})
.strip(); // Allow potential extra fields during parsing if needed, then validate structure
.strip(); // Enforce the canonical task shape and drop unknown fields
// Preprocessing schema that adds defaults before validation
const preprocessTaskSchema = z.preprocess((task) => {

View File

@@ -126,16 +126,6 @@ export class BaseAIProvider {
throw new Error('getRequiredApiKeyName must be implemented by provider');
}
/**
* Determines if a model requires max_completion_tokens instead of maxTokens
* Can be overridden by providers to specify their model requirements
* @param {string} modelId - The model ID to check
* @returns {boolean} True if the model requires max_completion_tokens
*/
requiresMaxCompletionTokens(modelId) {
return false; // Default behavior - most models use maxTokens
}
/**
* Prepares token limit parameter based on model requirements
* @param {string} modelId - The model ID
@@ -150,11 +140,7 @@ export class BaseAIProvider {
// Ensure maxTokens is an integer
const tokenValue = Math.floor(Number(maxTokens));
if (this.requiresMaxCompletionTokens(modelId)) {
return { max_completion_tokens: tokenValue };
} else {
return { maxTokens: tokenValue };
}
return { maxOutputTokens: tokenValue };
}
/**
@@ -248,7 +234,7 @@ export class BaseAIProvider {
messages: params.messages,
schema: zodSchema(params.schema),
mode: params.mode || 'auto',
maxTokens: params.maxTokens,
maxOutputTokens: params.maxTokens,
temperature: params.temperature
});

View File

@@ -1,54 +1,125 @@
/**
* src/ai-providers/claude-code.js
*
* Implementation for interacting with Claude models via Claude Code CLI
* using a custom AI SDK implementation.
* Claude Code provider implementation using the ai-sdk-provider-claude-code package.
* This provider uses the local Claude Code CLI with OAuth token authentication.
*
* Authentication:
* - Uses CLAUDE_CODE_OAUTH_TOKEN managed by Claude Code CLI
* - Token is set up via: claude setup-token
* - No manual API key configuration required
*/
import { createClaudeCode } from './custom-sdk/claude-code/index.js';
import { createClaudeCode } from 'ai-sdk-provider-claude-code';
import { BaseAIProvider } from './base-provider.js';
import { getClaudeCodeSettingsForCommand } from '../../scripts/modules/config-manager.js';
import { execSync } from 'child_process';
import { log } from '../../scripts/modules/utils.js';
let _claudeCliChecked = false;
let _claudeCliAvailable = null;
/**
* Provider for Claude Code CLI integration via AI SDK
*
* Features:
* - No API key required (uses local Claude Code CLI)
* - Supports 'sonnet' and 'opus' models
* - Command-specific configuration support
*/
export class ClaudeCodeProvider extends BaseAIProvider {
constructor() {
super();
this.name = 'Claude Code';
this.supportedModels = ['sonnet', 'opus'];
}
/**
* @returns {string} The environment variable name for API key (not used)
*/
getRequiredApiKeyName() {
return 'CLAUDE_CODE_API_KEY';
}
/**
* @returns {boolean} False - Claude Code doesn't require API keys
*/
isRequiredApiKey() {
return false;
}
/**
* Override validateAuth to skip API key validation for Claude Code
* @param {object} params - Parameters to validate
* Optional CLI availability check for Claude Code
* @param {object} params - Parameters (ignored)
*/
validateAuth(params) {
// Claude Code doesn't require an API key
// No validation needed
// Claude Code uses local CLI - perform lightweight availability check
// This is optional validation that fails fast with actionable guidance
if (
process.env.NODE_ENV !== 'test' &&
!_claudeCliChecked &&
!process.env.CLAUDE_CODE_OAUTH_TOKEN
) {
try {
execSync('claude --version', { stdio: 'pipe', timeout: 1000 });
_claudeCliAvailable = true;
} catch (error) {
_claudeCliAvailable = false;
log(
'warn',
'Claude Code CLI not detected. Install it with: npm install -g @anthropic-ai/claude-code'
);
} finally {
_claudeCliChecked = true;
}
}
}
/**
* Creates and returns a Claude Code client instance.
* @param {object} params - Parameters for client initialization
* @param {string} [params.commandName] - Name of the command invoking the service
* @param {string} [params.baseURL] - Optional custom API endpoint (not used by Claude Code)
* @returns {Function} Claude Code client function
* @throws {Error} If initialization fails
* Creates a Claude Code client instance
* @param {object} params - Client parameters
* @param {string} [params.commandName] - Command name for settings lookup
* @returns {Function} Claude Code provider function
* @throws {Error} If Claude Code CLI is not available or client creation fails
*/
getClient(params) {
getClient(params = {}) {
try {
// Claude Code doesn't use API keys or base URLs
// Just return the provider factory
const settings =
getClaudeCodeSettingsForCommand(params.commandName) || {};
return createClaudeCode({
defaultSettings: getClaudeCodeSettingsForCommand(params?.commandName)
defaultSettings: settings
});
} catch (error) {
this.handleError('client initialization', error);
// Provide more helpful error message
const msg = String(error?.message || '');
const code = error?.code;
if (code === 'ENOENT' || /claude/i.test(msg)) {
const enhancedError = new Error(
`Claude Code CLI not available. Please install Claude Code CLI first. Original error: ${error.message}`
);
enhancedError.cause = error;
this.handleError('Claude Code CLI initialization', enhancedError);
} else {
this.handleError('client initialization', error);
}
}
}
/**
* @returns {string[]} List of supported model IDs
*/
getSupportedModels() {
return this.supportedModels;
}
/**
* Check if a model is supported
* @param {string} modelId - Model ID to check
* @returns {boolean} True if supported
*/
isModelSupported(modelId) {
if (!modelId) return false;
return this.supportedModels.includes(String(modelId).toLowerCase());
}
}

View File

@@ -1,126 +0,0 @@
/**
* @fileoverview Error handling utilities for Claude Code provider
*/
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
/**
* @typedef {import('./types.js').ClaudeCodeErrorMetadata} ClaudeCodeErrorMetadata
*/
/**
* Create an API call error with Claude Code specific metadata
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @param {string} [params.code] - Error code
* @param {number} [params.exitCode] - Process exit code
* @param {string} [params.stderr] - Standard error output
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
* @param {boolean} [params.isRetryable=false] - Whether the error is retryable
* @returns {APICallError}
*/
export function createAPICallError({
message,
code,
exitCode,
stderr,
promptExcerpt,
isRetryable = false
}) {
/** @type {ClaudeCodeErrorMetadata} */
const metadata = {
code,
exitCode,
stderr,
promptExcerpt
};
return new APICallError({
message,
isRetryable,
url: 'claude-code-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Create an authentication error
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @returns {LoadAPIKeyError}
*/
export function createAuthenticationError({ message }) {
return new LoadAPIKeyError({
message:
message ||
'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
});
}
/**
* Create a timeout error
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
* @param {number} params.timeoutMs - Timeout in milliseconds
* @returns {APICallError}
*/
export function createTimeoutError({ message, promptExcerpt, timeoutMs }) {
// Store timeoutMs in metadata for potential use by error handlers
/** @type {ClaudeCodeErrorMetadata & { timeoutMs: number }} */
const metadata = {
code: 'TIMEOUT',
promptExcerpt,
timeoutMs
};
return new APICallError({
message,
isRetryable: true,
url: 'claude-code-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Check if an error is an authentication error
* @param {unknown} error - Error to check
* @returns {boolean}
*/
export function isAuthenticationError(error) {
if (error instanceof LoadAPIKeyError) return true;
if (
error instanceof APICallError &&
/** @type {ClaudeCodeErrorMetadata} */ (error.data)?.exitCode === 401
)
return true;
return false;
}
/**
* Check if an error is a timeout error
* @param {unknown} error - Error to check
* @returns {boolean}
*/
export function isTimeoutError(error) {
if (
error instanceof APICallError &&
/** @type {ClaudeCodeErrorMetadata} */ (error.data)?.code === 'TIMEOUT'
)
return true;
return false;
}
/**
* Get error metadata from an error
* @param {unknown} error - Error to extract metadata from
* @returns {ClaudeCodeErrorMetadata|undefined}
*/
export function getErrorMetadata(error) {
if (error instanceof APICallError && error.data) {
return /** @type {ClaudeCodeErrorMetadata} */ (error.data);
}
return undefined;
}

View File

@@ -1,83 +0,0 @@
/**
* @fileoverview Claude Code provider factory and exports
*/
import { NoSuchModelError } from '@ai-sdk/provider';
import { ClaudeCodeLanguageModel } from './language-model.js';
/**
* @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings
* @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId
* @typedef {import('./types.js').ClaudeCodeProvider} ClaudeCodeProvider
* @typedef {import('./types.js').ClaudeCodeProviderSettings} ClaudeCodeProviderSettings
*/
/**
* Create a Claude Code provider using the official SDK
* @param {ClaudeCodeProviderSettings} [options={}] - Provider configuration options
* @returns {ClaudeCodeProvider} Claude Code provider instance
*/
export function createClaudeCode(options = {}) {
/**
* Create a language model instance
* @param {ClaudeCodeModelId} modelId - Model ID
* @param {ClaudeCodeSettings} [settings={}] - Model settings
* @returns {ClaudeCodeLanguageModel}
*/
const createModel = (modelId, settings = {}) => {
return new ClaudeCodeLanguageModel({
id: modelId,
settings: {
...options.defaultSettings,
...settings
}
});
};
/**
* Provider function
* @param {ClaudeCodeModelId} modelId - Model ID
* @param {ClaudeCodeSettings} [settings] - Model settings
* @returns {ClaudeCodeLanguageModel}
*/
const provider = function (modelId, settings) {
if (new.target) {
throw new Error(
'The Claude Code model function cannot be called with the new keyword.'
);
}
return createModel(modelId, settings);
};
provider.languageModel = createModel;
provider.chat = createModel; // Alias for languageModel
// Add textEmbeddingModel method that throws NoSuchModelError
provider.textEmbeddingModel = (modelId) => {
throw new NoSuchModelError({
modelId,
modelType: 'textEmbeddingModel'
});
};
return /** @type {ClaudeCodeProvider} */ (provider);
}
/**
* Default Claude Code provider instance
*/
export const claudeCode = createClaudeCode();
// Provider exports
export { ClaudeCodeLanguageModel } from './language-model.js';
// Error handling exports
export {
isAuthenticationError,
isTimeoutError,
getErrorMetadata,
createAPICallError,
createAuthenticationError,
createTimeoutError
} from './errors.js';

View File

@@ -1,59 +0,0 @@
/**
* @fileoverview Extract JSON from Claude's response, handling markdown blocks and other formatting
*/
/**
* Extract JSON from Claude's response
* @param {string} text - The text to extract JSON from
* @returns {string} - The extracted JSON string
*/
export function extractJson(text) {
// Remove markdown code blocks if present
let jsonText = text.trim();
// Remove ```json blocks
jsonText = jsonText.replace(/^```json\s*/gm, '');
jsonText = jsonText.replace(/^```\s*/gm, '');
jsonText = jsonText.replace(/```\s*$/gm, '');
// Remove common TypeScript/JavaScript patterns
jsonText = jsonText.replace(/^const\s+\w+\s*=\s*/, ''); // Remove "const varName = "
jsonText = jsonText.replace(/^let\s+\w+\s*=\s*/, ''); // Remove "let varName = "
jsonText = jsonText.replace(/^var\s+\w+\s*=\s*/, ''); // Remove "var varName = "
jsonText = jsonText.replace(/;?\s*$/, ''); // Remove trailing semicolons
// Try to extract JSON object or array
const objectMatch = jsonText.match(/{[\s\S]*}/);
const arrayMatch = jsonText.match(/\[[\s\S]*\]/);
if (objectMatch) {
jsonText = objectMatch[0];
} else if (arrayMatch) {
jsonText = arrayMatch[0];
}
// First try to parse as valid JSON
try {
JSON.parse(jsonText);
return jsonText;
} catch {
// If it's not valid JSON, it might be a JavaScript object literal
// Try to convert it to valid JSON
try {
// This is a simple conversion that handles basic cases
// Replace unquoted keys with quoted keys
const converted = jsonText
.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
// Replace single quotes with double quotes
.replace(/'/g, '"');
// Validate the converted JSON
JSON.parse(converted);
return converted;
} catch {
// If all else fails, return the original text
// The AI SDK will handle the error appropriately
return text;
}
}
}

View File

@@ -1,540 +0,0 @@
/**
* @fileoverview Claude Code Language Model implementation
*/
import { NoSuchModelError } from '@ai-sdk/provider';
import { generateId } from '@ai-sdk/provider-utils';
import { convertToClaudeCodeMessages } from './message-converter.js';
import { extractJson } from './json-extractor.js';
import { createAPICallError, createAuthenticationError } from './errors.js';
let query;
let AbortError;
async function loadClaudeCodeModule() {
if (!query || !AbortError) {
try {
const mod = await import('@anthropic-ai/claude-code');
query = mod.query;
AbortError = mod.AbortError;
} catch (err) {
throw new Error(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
}
}
}
/**
* @typedef {import('./types.js').ClaudeCodeSettings} ClaudeCodeSettings
* @typedef {import('./types.js').ClaudeCodeModelId} ClaudeCodeModelId
* @typedef {import('./types.js').ClaudeCodeLanguageModelOptions} ClaudeCodeLanguageModelOptions
*/
const modelMap = {
opus: 'opus',
sonnet: 'sonnet'
};
export class ClaudeCodeLanguageModel {
specificationVersion = 'v1';
defaultObjectGenerationMode = 'json';
supportsImageUrls = false;
supportsStructuredOutputs = false;
/** @type {ClaudeCodeModelId} */
modelId;
/** @type {ClaudeCodeSettings} */
settings;
/** @type {string|undefined} */
sessionId;
/**
* @param {ClaudeCodeLanguageModelOptions} options
*/
constructor(options) {
this.modelId = options.id;
this.settings = options.settings ?? {};
// Validate model ID format
if (
!this.modelId ||
typeof this.modelId !== 'string' ||
this.modelId.trim() === ''
) {
throw new NoSuchModelError({
modelId: this.modelId,
modelType: 'languageModel'
});
}
}
get provider() {
return 'claude-code';
}
/**
* Get the model name for Claude Code CLI
* @returns {string}
*/
getModel() {
const mapped = modelMap[this.modelId];
return mapped ?? this.modelId;
}
/**
* Generate unsupported parameter warnings
* @param {Object} options - Generation options
* @returns {Array} Warnings array
*/
generateUnsupportedWarnings(options) {
const warnings = [];
const unsupportedParams = [];
// Check for unsupported parameters
if (options.temperature !== undefined)
unsupportedParams.push('temperature');
if (options.maxTokens !== undefined) unsupportedParams.push('maxTokens');
if (options.topP !== undefined) unsupportedParams.push('topP');
if (options.topK !== undefined) unsupportedParams.push('topK');
if (options.presencePenalty !== undefined)
unsupportedParams.push('presencePenalty');
if (options.frequencyPenalty !== undefined)
unsupportedParams.push('frequencyPenalty');
if (options.stopSequences !== undefined && options.stopSequences.length > 0)
unsupportedParams.push('stopSequences');
if (options.seed !== undefined) unsupportedParams.push('seed');
if (unsupportedParams.length > 0) {
// Add a warning for each unsupported parameter
for (const param of unsupportedParams) {
warnings.push({
type: 'unsupported-setting',
setting: param,
details: `Claude Code CLI does not support the ${param} parameter. It will be ignored.`
});
}
}
return warnings;
}
/**
* Generate text using Claude Code
* @param {Object} options - Generation options
* @returns {Promise<Object>}
*/
async doGenerate(options) {
await loadClaudeCodeModule();
const { messagesPrompt } = convertToClaudeCodeMessages(
options.prompt,
options.mode
);
const abortController = new AbortController();
if (options.abortSignal) {
options.abortSignal.addEventListener('abort', () =>
abortController.abort()
);
}
const queryOptions = {
model: this.getModel(),
abortController,
resume: this.sessionId,
pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
customSystemPrompt: this.settings.customSystemPrompt,
appendSystemPrompt: this.settings.appendSystemPrompt,
maxTurns: this.settings.maxTurns,
maxThinkingTokens: this.settings.maxThinkingTokens,
cwd: this.settings.cwd,
executable: this.settings.executable,
executableArgs: this.settings.executableArgs,
permissionMode: this.settings.permissionMode,
permissionPromptToolName: this.settings.permissionPromptToolName,
continue: this.settings.continue,
allowedTools: this.settings.allowedTools,
disallowedTools: this.settings.disallowedTools,
mcpServers: this.settings.mcpServers
};
let text = '';
let usage = { promptTokens: 0, completionTokens: 0 };
let finishReason = 'stop';
let costUsd;
let durationMs;
let rawUsage;
const warnings = this.generateUnsupportedWarnings(options);
try {
if (!query) {
throw new Error(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
}
const response = query({
prompt: messagesPrompt,
options: queryOptions
});
for await (const message of response) {
if (message.type === 'assistant') {
text += message.message.content
.map((c) => (c.type === 'text' ? c.text : ''))
.join('');
} else if (message.type === 'result') {
this.sessionId = message.session_id;
costUsd = message.total_cost_usd;
durationMs = message.duration_ms;
if ('usage' in message) {
rawUsage = message.usage;
usage = {
promptTokens:
(message.usage.cache_creation_input_tokens ?? 0) +
(message.usage.cache_read_input_tokens ?? 0) +
(message.usage.input_tokens ?? 0),
completionTokens: message.usage.output_tokens ?? 0
};
}
if (message.subtype === 'error_max_turns') {
finishReason = 'length';
} else if (message.subtype === 'error_during_execution') {
finishReason = 'error';
}
} else if (message.type === 'system' && message.subtype === 'init') {
this.sessionId = message.session_id;
}
}
} catch (error) {
// -------------------------------------------------------------
// Work-around for Claude-Code CLI/SDK JSON truncation bug (#913)
// -------------------------------------------------------------
// If the SDK throws a JSON SyntaxError *but* we already hold some
// buffered text, assume the response was truncated by the CLI.
// We keep the accumulated text, mark the finish reason, push a
// provider-warning and *skip* the normal error handling so Task
// Master can continue processing.
const isJsonTruncation =
error instanceof SyntaxError &&
/JSON/i.test(error.message || '') &&
(error.message.includes('position') ||
error.message.includes('Unexpected end'));
if (isJsonTruncation && text && text.length > 0) {
warnings.push({
type: 'provider-warning',
details:
'Claude Code SDK emitted a JSON parse error but Task Master recovered buffered text (possible CLI truncation).'
});
finishReason = 'truncated';
// Skip re-throwing: fall through so the caller receives usable data
} else {
if (AbortError && error instanceof AbortError) {
throw options.abortSignal?.aborted
? options.abortSignal.reason
: error;
}
// Check for authentication errors
if (
error.message?.includes('not logged in') ||
error.message?.includes('authentication') ||
error.exitCode === 401
) {
throw createAuthenticationError({
message:
error.message ||
'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
});
}
// Wrap other errors with API call error
throw createAPICallError({
message: error.message || 'Claude Code CLI error',
code: error.code,
exitCode: error.exitCode,
stderr: error.stderr,
promptExcerpt: messagesPrompt.substring(0, 200),
isRetryable: error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
});
}
}
// Extract JSON if in object-json mode
if (options.mode?.type === 'object-json' && text) {
text = extractJson(text);
}
return {
text: text || undefined,
usage,
finishReason,
rawCall: {
rawPrompt: messagesPrompt,
rawSettings: queryOptions
},
warnings: warnings.length > 0 ? warnings : undefined,
response: {
id: generateId(),
timestamp: new Date(),
modelId: this.modelId
},
request: {
body: messagesPrompt
},
providerMetadata: {
'claude-code': {
...(this.sessionId !== undefined && { sessionId: this.sessionId }),
...(costUsd !== undefined && { costUsd }),
...(durationMs !== undefined && { durationMs }),
...(rawUsage !== undefined && { rawUsage })
}
}
};
}
/**
* Stream text using Claude Code
* @param {Object} options - Stream options
* @returns {Promise<Object>}
*/
async doStream(options) {
await loadClaudeCodeModule();
const { messagesPrompt } = convertToClaudeCodeMessages(
options.prompt,
options.mode
);
const abortController = new AbortController();
if (options.abortSignal) {
options.abortSignal.addEventListener('abort', () =>
abortController.abort()
);
}
const queryOptions = {
model: this.getModel(),
abortController,
resume: this.sessionId,
pathToClaudeCodeExecutable: this.settings.pathToClaudeCodeExecutable,
customSystemPrompt: this.settings.customSystemPrompt,
appendSystemPrompt: this.settings.appendSystemPrompt,
maxTurns: this.settings.maxTurns,
maxThinkingTokens: this.settings.maxThinkingTokens,
cwd: this.settings.cwd,
executable: this.settings.executable,
executableArgs: this.settings.executableArgs,
permissionMode: this.settings.permissionMode,
permissionPromptToolName: this.settings.permissionPromptToolName,
continue: this.settings.continue,
allowedTools: this.settings.allowedTools,
disallowedTools: this.settings.disallowedTools,
mcpServers: this.settings.mcpServers
};
const warnings = this.generateUnsupportedWarnings(options);
const stream = new ReadableStream({
start: async (controller) => {
try {
if (!query) {
throw new Error(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
}
const response = query({
prompt: messagesPrompt,
options: queryOptions
});
let usage = { promptTokens: 0, completionTokens: 0 };
let accumulatedText = '';
for await (const message of response) {
if (message.type === 'assistant') {
const text = message.message.content
.map((c) => (c.type === 'text' ? c.text : ''))
.join('');
if (text) {
accumulatedText += text;
// In object-json mode, we need to accumulate the full text
// and extract JSON at the end, so don't stream individual deltas
if (options.mode?.type !== 'object-json') {
controller.enqueue({
type: 'text-delta',
textDelta: text
});
}
}
} else if (message.type === 'result') {
let rawUsage;
if ('usage' in message) {
rawUsage = message.usage;
usage = {
promptTokens:
(message.usage.cache_creation_input_tokens ?? 0) +
(message.usage.cache_read_input_tokens ?? 0) +
(message.usage.input_tokens ?? 0),
completionTokens: message.usage.output_tokens ?? 0
};
}
let finishReason = 'stop';
if (message.subtype === 'error_max_turns') {
finishReason = 'length';
} else if (message.subtype === 'error_during_execution') {
finishReason = 'error';
}
// Store session ID in the model instance
this.sessionId = message.session_id;
// In object-json mode, extract JSON and send the full text at once
if (options.mode?.type === 'object-json' && accumulatedText) {
const extractedJson = extractJson(accumulatedText);
controller.enqueue({
type: 'text-delta',
textDelta: extractedJson
});
}
controller.enqueue({
type: 'finish',
finishReason,
usage,
providerMetadata: {
'claude-code': {
sessionId: message.session_id,
...(message.total_cost_usd !== undefined && {
costUsd: message.total_cost_usd
}),
...(message.duration_ms !== undefined && {
durationMs: message.duration_ms
}),
...(rawUsage !== undefined && { rawUsage })
}
}
});
} else if (
message.type === 'system' &&
message.subtype === 'init'
) {
// Store session ID for future use
this.sessionId = message.session_id;
// Emit response metadata when session is initialized
controller.enqueue({
type: 'response-metadata',
id: message.session_id,
timestamp: new Date(),
modelId: this.modelId
});
}
}
// -------------------------------------------------------------
// Work-around for Claude-Code CLI/SDK JSON truncation bug (#913)
// -------------------------------------------------------------
// If we hit the SDK JSON SyntaxError but have buffered text, finalize
// the stream gracefully instead of emitting an error.
const isJsonTruncation =
error instanceof SyntaxError &&
/JSON/i.test(error.message || '') &&
(error.message.includes('position') ||
error.message.includes('Unexpected end'));
if (
isJsonTruncation &&
accumulatedText &&
accumulatedText.length > 0
) {
// Prepare final text payload
const finalText =
options.mode?.type === 'object-json'
? extractJson(accumulatedText)
: accumulatedText;
// Emit any remaining text
controller.enqueue({
type: 'text-delta',
textDelta: finalText
});
// Emit finish with truncated reason and warning
controller.enqueue({
type: 'finish',
finishReason: 'truncated',
usage,
providerMetadata: { 'claude-code': { truncated: true } },
warnings: [
{
type: 'provider-warning',
details:
'Claude Code SDK JSON truncation detected; stream recovered.'
}
]
});
controller.close();
return; // Skip normal error path
}
controller.close();
} catch (error) {
let errorToEmit;
if (AbortError && error instanceof AbortError) {
errorToEmit = options.abortSignal?.aborted
? options.abortSignal.reason
: error;
} else if (
error.message?.includes('not logged in') ||
error.message?.includes('authentication') ||
error.exitCode === 401
) {
errorToEmit = createAuthenticationError({
message:
error.message ||
'Authentication failed. Please ensure Claude Code CLI is properly authenticated.'
});
} else {
errorToEmit = createAPICallError({
message: error.message || 'Claude Code CLI error',
code: error.code,
exitCode: error.exitCode,
stderr: error.stderr,
promptExcerpt: messagesPrompt.substring(0, 200),
isRetryable:
error.code === 'ENOENT' || error.code === 'ECONNREFUSED'
});
}
// Emit error as a stream part
controller.enqueue({
type: 'error',
error: errorToEmit
});
controller.close();
}
}
});
return {
stream,
rawCall: {
rawPrompt: messagesPrompt,
rawSettings: queryOptions
},
warnings: warnings.length > 0 ? warnings : undefined,
request: {
body: messagesPrompt
}
};
}
}

View File

@@ -1,139 +0,0 @@
/**
* @fileoverview Converts AI SDK prompt format to Claude Code message format
*/
/**
* Convert AI SDK prompt to Claude Code messages format
* @param {Array} prompt - AI SDK prompt array
* @param {Object} [mode] - Generation mode
* @param {string} mode.type - Mode type ('regular', 'object-json', 'object-tool')
* @returns {{messagesPrompt: string, systemPrompt?: string}}
*/
export function convertToClaudeCodeMessages(prompt, mode) {
const messages = [];
let systemPrompt;
for (const message of prompt) {
switch (message.role) {
case 'system':
systemPrompt = message.content;
break;
case 'user':
if (typeof message.content === 'string') {
messages.push(message.content);
} else {
// Handle multi-part content
const textParts = message.content
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('\n');
if (textParts) {
messages.push(textParts);
}
// Note: Image parts are not supported by Claude Code CLI
const imageParts = message.content.filter(
(part) => part.type === 'image'
);
if (imageParts.length > 0) {
console.warn(
'Claude Code CLI does not support image inputs. Images will be ignored.'
);
}
}
break;
case 'assistant':
if (typeof message.content === 'string') {
messages.push(`Assistant: ${message.content}`);
} else {
const textParts = message.content
.filter((part) => part.type === 'text')
.map((part) => part.text)
.join('\n');
if (textParts) {
messages.push(`Assistant: ${textParts}`);
}
// Handle tool calls if present
const toolCalls = message.content.filter(
(part) => part.type === 'tool-call'
);
if (toolCalls.length > 0) {
// For now, we'll just note that tool calls were made
messages.push(`Assistant: [Tool calls made]`);
}
}
break;
case 'tool':
// Tool results could be included in the conversation
messages.push(
`Tool Result (${message.content[0].toolName}): ${JSON.stringify(
message.content[0].result
)}`
);
break;
}
}
// For the SDK, we need to provide a single prompt string
// Format the conversation history properly
// Combine system prompt with messages
let finalPrompt = '';
// Add system prompt at the beginning if present
if (systemPrompt) {
finalPrompt = systemPrompt;
}
if (messages.length === 0) {
return { messagesPrompt: finalPrompt, systemPrompt };
}
// Format messages
const formattedMessages = [];
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
// Check if this is a user or assistant message based on content
if (msg.startsWith('Assistant:') || msg.startsWith('Tool Result')) {
formattedMessages.push(msg);
} else {
// User messages
formattedMessages.push(`Human: ${msg}`);
}
}
// Combine system prompt with messages
if (finalPrompt) {
finalPrompt = finalPrompt + '\n\n' + formattedMessages.join('\n\n');
} else {
finalPrompt = formattedMessages.join('\n\n');
}
// For JSON mode, add explicit instruction to ensure JSON output
if (mode?.type === 'object-json') {
// Make the JSON instruction even more explicit
finalPrompt = `${finalPrompt}
CRITICAL INSTRUCTION: You MUST respond with ONLY valid JSON. Follow these rules EXACTLY:
1. Start your response with an opening brace {
2. End your response with a closing brace }
3. Do NOT include any text before the opening brace
4. Do NOT include any text after the closing brace
5. Do NOT use markdown code blocks or backticks
6. Do NOT include explanations or commentary
7. The ENTIRE response must be valid JSON that can be parsed with JSON.parse()
Begin your response with { and end with }`;
}
return {
messagesPrompt: finalPrompt,
systemPrompt
};
}

View File

@@ -1,73 +0,0 @@
/**
* @fileoverview Type definitions for Claude Code AI SDK provider
* These JSDoc types mirror the TypeScript interfaces from the original provider
*/
/**
* Claude Code provider settings
* @typedef {Object} ClaudeCodeSettings
* @property {string} [pathToClaudeCodeExecutable='claude'] - Custom path to Claude Code CLI executable
* @property {string} [customSystemPrompt] - Custom system prompt to use
* @property {string} [appendSystemPrompt] - Append additional content to the system prompt
* @property {number} [maxTurns] - Maximum number of turns for the conversation
* @property {number} [maxThinkingTokens] - Maximum thinking tokens for the model
* @property {string} [cwd] - Working directory for CLI operations
* @property {'bun'|'deno'|'node'} [executable='node'] - JavaScript runtime to use
* @property {string[]} [executableArgs] - Additional arguments for the JavaScript runtime
* @property {'default'|'acceptEdits'|'bypassPermissions'|'plan'} [permissionMode='default'] - Permission mode for tool usage
* @property {string} [permissionPromptToolName] - Custom tool name for permission prompts
* @property {boolean} [continue] - Continue the most recent conversation
* @property {string} [resume] - Resume a specific session by ID
* @property {string[]} [allowedTools] - Tools to explicitly allow during execution (e.g., ['Read', 'LS', 'Bash(git log:*)'])
* @property {string[]} [disallowedTools] - Tools to disallow during execution (e.g., ['Write', 'Edit', 'Bash(rm:*)'])
* @property {Object.<string, MCPServerConfig>} [mcpServers] - MCP server configuration
* @property {boolean} [verbose] - Enable verbose logging for debugging
*/
/**
* MCP Server configuration
* @typedef {Object} MCPServerConfig
* @property {'stdio'|'sse'} [type='stdio'] - Server type
* @property {string} command - Command to execute (for stdio type)
* @property {string[]} [args] - Arguments for the command
* @property {Object.<string, string>} [env] - Environment variables
* @property {string} url - URL for SSE type servers
* @property {Object.<string, string>} [headers] - Headers for SSE type servers
*/
/**
* Model ID type - either 'opus', 'sonnet', or any string
* @typedef {'opus'|'sonnet'|string} ClaudeCodeModelId
*/
/**
* Language model options
* @typedef {Object} ClaudeCodeLanguageModelOptions
* @property {ClaudeCodeModelId} id - The model ID
* @property {ClaudeCodeSettings} [settings] - Optional settings
*/
/**
* Error metadata for Claude Code errors
* @typedef {Object} ClaudeCodeErrorMetadata
* @property {string} [code] - Error code
* @property {number} [exitCode] - Process exit code
* @property {string} [stderr] - Standard error output
* @property {string} [promptExcerpt] - Excerpt of the prompt that caused the error
*/
/**
* Claude Code provider interface
* @typedef {Object} ClaudeCodeProvider
* @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} languageModel - Create a language model
* @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} chat - Alias for languageModel
* @property {function(string): never} textEmbeddingModel - Throws NoSuchModelError (not supported)
*/
/**
* Claude Code provider settings
* @typedef {Object} ClaudeCodeProviderSettings
* @property {ClaudeCodeSettings} [defaultSettings] - Default settings to use for all models
*/
export {}; // This ensures the file is treated as a module

View File

@@ -1,155 +0,0 @@
/**
* @fileoverview Error handling utilities for Grok CLI provider
*/
import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
/**
* @typedef {import('./types.js').GrokCliErrorMetadata} GrokCliErrorMetadata
*/
/**
* Create an API call error with Grok CLI specific metadata
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @param {string} [params.code] - Error code
* @param {number} [params.exitCode] - Process exit code
* @param {string} [params.stderr] - Standard error output
* @param {string} [params.stdout] - Standard output
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
* @param {boolean} [params.isRetryable=false] - Whether the error is retryable
* @returns {APICallError}
*/
export function createAPICallError({
message,
code,
exitCode,
stderr,
stdout,
promptExcerpt,
isRetryable = false
}) {
/** @type {GrokCliErrorMetadata} */
const metadata = {
code,
exitCode,
stderr,
stdout,
promptExcerpt
};
return new APICallError({
message,
isRetryable,
url: 'grok-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Create an authentication error
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @returns {LoadAPIKeyError}
*/
export function createAuthenticationError({ message }) {
return new LoadAPIKeyError({
message:
message ||
'Authentication failed. Please ensure Grok CLI is properly configured with API key.'
});
}
/**
* Create a timeout error
* @param {Object} params - Error parameters
* @param {string} params.message - Error message
* @param {string} [params.promptExcerpt] - Excerpt of the prompt
* @param {number} params.timeoutMs - Timeout in milliseconds
* @returns {APICallError}
*/
export function createTimeoutError({ message, promptExcerpt, timeoutMs }) {
/** @type {GrokCliErrorMetadata & { timeoutMs: number }} */
const metadata = {
code: 'TIMEOUT',
promptExcerpt,
timeoutMs
};
return new APICallError({
message,
isRetryable: true,
url: 'grok-cli://command',
requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined,
data: metadata
});
}
/**
* Create a CLI installation error
* @param {Object} params - Error parameters
* @param {string} [params.message] - Error message
* @returns {APICallError}
*/
export function createInstallationError({ message }) {
return new APICallError({
message:
message ||
'Grok CLI is not installed or not found in PATH. Please install with: npm install -g @vibe-kit/grok-cli',
isRetryable: false,
url: 'grok-cli://installation'
});
}
/**
* Check if an error is an authentication error
* @param {unknown} error - Error to check
* @returns {boolean}
*/
export function isAuthenticationError(error) {
if (error instanceof LoadAPIKeyError) return true;
if (
error instanceof APICallError &&
/** @type {GrokCliErrorMetadata} */ (error.data)?.exitCode === 401
)
return true;
return false;
}
/**
* Check if an error is a timeout error
* @param {unknown} error - Error to check
* @returns {boolean}
*/
export function isTimeoutError(error) {
if (
error instanceof APICallError &&
/** @type {GrokCliErrorMetadata} */ (error.data)?.code === 'TIMEOUT'
)
return true;
return false;
}
/**
* Check if an error is an installation error
* @param {unknown} error - Error to check
* @returns {boolean}
*/
export function isInstallationError(error) {
if (error instanceof APICallError && error.url === 'grok-cli://installation')
return true;
return false;
}
/**
* Get error metadata from an error
* @param {unknown} error - Error to extract metadata from
* @returns {GrokCliErrorMetadata|undefined}
*/
export function getErrorMetadata(error) {
if (error instanceof APICallError && error.data) {
return /** @type {GrokCliErrorMetadata} */ (error.data);
}
return undefined;
}

View File

@@ -1,85 +0,0 @@
/**
* @fileoverview Grok CLI provider factory and exports
*/
import { NoSuchModelError } from '@ai-sdk/provider';
import { GrokCliLanguageModel } from './language-model.js';
/**
* @typedef {import('./types.js').GrokCliSettings} GrokCliSettings
* @typedef {import('./types.js').GrokCliModelId} GrokCliModelId
* @typedef {import('./types.js').GrokCliProvider} GrokCliProvider
* @typedef {import('./types.js').GrokCliProviderSettings} GrokCliProviderSettings
*/
/**
* Create a Grok CLI provider
* @param {GrokCliProviderSettings} [options={}] - Provider configuration options
* @returns {GrokCliProvider} Grok CLI provider instance
*/
export function createGrokCli(options = {}) {
/**
* Create a language model instance
* @param {GrokCliModelId} modelId - Model ID
* @param {GrokCliSettings} [settings={}] - Model settings
* @returns {GrokCliLanguageModel}
*/
const createModel = (modelId, settings = {}) => {
return new GrokCliLanguageModel({
id: modelId,
settings: {
...options.defaultSettings,
...settings
}
});
};
/**
* Provider function
* @param {GrokCliModelId} modelId - Model ID
* @param {GrokCliSettings} [settings] - Model settings
* @returns {GrokCliLanguageModel}
*/
const provider = function (modelId, settings) {
if (new.target) {
throw new Error(
'The Grok CLI model function cannot be called with the new keyword.'
);
}
return createModel(modelId, settings);
};
provider.languageModel = createModel;
provider.chat = createModel; // Alias for languageModel
// Add textEmbeddingModel method that throws NoSuchModelError
provider.textEmbeddingModel = (modelId) => {
throw new NoSuchModelError({
modelId,
modelType: 'textEmbeddingModel'
});
};
return /** @type {GrokCliProvider} */ (provider);
}
/**
* Default Grok CLI provider instance
*/
export const grokCli = createGrokCli();
// Provider exports
export { GrokCliLanguageModel } from './language-model.js';
// Error handling exports
export {
isAuthenticationError,
isTimeoutError,
isInstallationError,
getErrorMetadata,
createAPICallError,
createAuthenticationError,
createTimeoutError,
createInstallationError
} from './errors.js';

View File

@@ -1,59 +0,0 @@
/**
* @fileoverview Extract JSON from Grok's response, handling markdown blocks and other formatting
*/
/**
* Extract JSON from Grok's response
* @param {string} text - The text to extract JSON from
* @returns {string} - The extracted JSON string
*/
export function extractJson(text) {
// Remove markdown code blocks if present
let jsonText = text.trim();
// Remove ```json blocks
jsonText = jsonText.replace(/^```json\s*/gm, '');
jsonText = jsonText.replace(/^```\s*/gm, '');
jsonText = jsonText.replace(/```\s*$/gm, '');
// Remove common TypeScript/JavaScript patterns
jsonText = jsonText.replace(/^const\s+\w+\s*=\s*/, ''); // Remove "const varName = "
jsonText = jsonText.replace(/^let\s+\w+\s*=\s*/, ''); // Remove "let varName = "
jsonText = jsonText.replace(/^var\s+\w+\s*=\s*/, ''); // Remove "var varName = "
jsonText = jsonText.replace(/;?\s*$/, ''); // Remove trailing semicolons
// Try to extract JSON object or array
const objectMatch = jsonText.match(/{[\s\S]*}/);
const arrayMatch = jsonText.match(/\[[\s\S]*\]/);
if (objectMatch) {
jsonText = objectMatch[0];
} else if (arrayMatch) {
jsonText = arrayMatch[0];
}
// First try to parse as valid JSON
try {
JSON.parse(jsonText);
return jsonText;
} catch {
// If it's not valid JSON, it might be a JavaScript object literal
// Try to convert it to valid JSON
try {
// This is a simple conversion that handles basic cases
// Replace unquoted keys with quoted keys
const converted = jsonText
.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":')
// Replace single quotes with double quotes
.replace(/'/g, '"');
// Validate the converted JSON
JSON.parse(converted);
return converted;
} catch {
// If all else fails, return the original text
// The AI SDK will handle the error appropriately
return text;
}
}
}

View File

@@ -1,56 +0,0 @@
/**
* @fileoverview Type definitions for Grok CLI provider
*/
/**
* @typedef {Object} GrokCliSettings
* @property {string} [apiKey] - API key for Grok CLI
* @property {string} [baseURL] - Base URL for Grok API
* @property {string} [model] - Default model to use
* @property {number} [timeout] - Timeout in milliseconds
* @property {string} [workingDirectory] - Working directory for CLI commands
*/
/**
* @typedef {string} GrokCliModelId
* Model identifiers supported by Grok CLI
*/
/**
* @typedef {Object} GrokCliErrorMetadata
* @property {string} [code] - Error code
* @property {number} [exitCode] - Process exit code
* @property {string} [stderr] - Standard error output
* @property {string} [stdout] - Standard output
* @property {string} [promptExcerpt] - Excerpt of the prompt that caused the error
* @property {number} [timeoutMs] - Timeout value in milliseconds
*/
/**
* @typedef {Function} GrokCliProvider
* @property {Function} languageModel - Create a language model
* @property {Function} chat - Alias for languageModel
* @property {Function} textEmbeddingModel - Text embedding model (throws error)
*/
/**
* @typedef {Object} GrokCliProviderSettings
* @property {GrokCliSettings} [defaultSettings] - Default settings for all models
*/
/**
* @typedef {Object} GrokCliMessage
* @property {string} role - Message role (user, assistant, system)
* @property {string} content - Message content
*/
/**
* @typedef {Object} GrokCliResponse
* @property {string} content - Response content
* @property {Object} [usage] - Token usage information
* @property {number} [usage.prompt_tokens] - Input tokens used
* @property {number} [usage.completion_tokens] - Output tokens used
* @property {number} [usage.total_tokens] - Total tokens used
*/
export {};

View File

@@ -9,21 +9,7 @@ import { generateObject, generateText, streamText } from 'ai';
import { parse } from 'jsonc-parser';
import { BaseAIProvider } from './base-provider.js';
import { log } from '../../scripts/modules/utils.js';
let createGeminiProvider;
async function loadGeminiCliModule() {
if (!createGeminiProvider) {
try {
const mod = await import('ai-sdk-provider-gemini-cli');
createGeminiProvider = mod.createGeminiProvider;
} catch (err) {
throw new Error(
"Gemini CLI SDK is not installed. Please install 'ai-sdk-provider-gemini-cli' to use the gemini-cli provider."
);
}
}
}
import { createGeminiProvider } from 'ai-sdk-provider-gemini-cli';
export class GeminiCliProvider extends BaseAIProvider {
constructor() {
@@ -54,8 +40,6 @@ export class GeminiCliProvider extends BaseAIProvider {
*/
async getClient(params) {
try {
// Load the Gemini CLI module dynamically
await loadGeminiCliModule();
// Primary use case: Use existing gemini CLI authentication
// Secondary use case: Direct API key (for compatibility)
let authOptions = {};
@@ -441,7 +425,7 @@ Generate ${subtaskCount} subtasks based on the original task context. Return ONL
model: client(params.modelId),
system: systemPrompt,
messages: messages,
maxTokens: params.maxTokens,
maxOutputTokens: params.maxTokens,
temperature: params.temperature
});
@@ -545,7 +529,7 @@ Generate ${subtaskCount} subtasks based on the original task context. Return ONL
model: client(params.modelId),
system: systemPrompt,
messages: messages,
maxTokens: params.maxTokens,
maxOutputTokens: params.maxTokens,
temperature: params.temperature
});
@@ -604,7 +588,7 @@ Generate ${subtaskCount} subtasks based on the original task context. Return ONL
messages: messages,
schema: params.schema,
mode: 'json', // Use json mode instead of auto for Gemini
maxTokens: params.maxTokens,
maxOutputTokens: params.maxTokens,
temperature: params.temperature
});

View File

@@ -3,7 +3,7 @@
* AI provider implementation for Grok models using Grok CLI.
*/
import { createGrokCli } from './custom-sdk/grok-cli/index.js';
import { createGrokCli } from '@tm/ai-sdk-provider-grok-cli';
import { BaseAIProvider } from './base-provider.js';
import { getGrokCliSettingsForCommand } from '../../scripts/modules/config-manager.js';

View File

@@ -1,9 +1,9 @@
/**
* ollama.js
* AI provider implementation for Ollama models using the ollama-ai-provider package.
* AI provider implementation for Ollama models using the ollama-ai-provider-v2 package.
*/
import { createOllama } from 'ollama-ai-provider';
import { createOllama } from 'ollama-ai-provider-v2';
import { BaseAIProvider } from './base-provider.js';
export class OllamaAIProvider extends BaseAIProvider {

View File

@@ -20,16 +20,6 @@ export class OpenAIProvider extends BaseAIProvider {
return 'OPENAI_API_KEY';
}
/**
* Determines if a model requires max_completion_tokens instead of maxTokens
* GPT-5 models require max_completion_tokens parameter
* @param {string} modelId - The model ID to check
* @returns {boolean} True if the model requires max_completion_tokens
*/
requiresMaxCompletionTokens(modelId) {
return modelId && modelId.startsWith('gpt-5');
}
/**
* Creates and returns an OpenAI client instance.
* @param {object} params - Parameters for client initialization

View File

@@ -1,5 +1,134 @@
// Cursor conversion profile for rule-transformer
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';
import path from 'path';
import fs from 'fs';
import { log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';
// Helper copy; use cpSync when available, fallback to manual recursion
function copyRecursiveSync(src, dest) {
if (fs.cpSync) {
try {
fs.cpSync(src, dest, { recursive: true, force: true });
return;
} catch (err) {
throw new Error(`Failed to copy ${src} to ${dest}: ${err.message}`);
}
}
const exists = fs.existsSync(src);
let stats = null;
let isDirectory = false;
if (exists) {
try {
stats = fs.statSync(src);
isDirectory = stats.isDirectory();
} catch (err) {
// Handle TOCTOU race condition - treat as non-existent/not-a-directory
isDirectory = false;
}
}
if (isDirectory) {
try {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
for (const child of fs.readdirSync(src)) {
copyRecursiveSync(path.join(src, child), path.join(dest, child));
}
} catch (err) {
throw new Error(
`Failed to copy directory ${src} to ${dest}: ${err.message}`
);
}
} else {
try {
// ensure parent exists for file copies
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
} catch (err) {
throw new Error(`Failed to copy file ${src} to ${dest}: ${err.message}`);
}
}
}
// Helper function to recursively remove directory
function removeDirectoryRecursive(dirPath) {
if (fs.existsSync(dirPath)) {
try {
fs.rmSync(dirPath, { recursive: true, force: true });
return true;
} catch (err) {
log('error', `Failed to remove directory ${dirPath}: ${err.message}`);
return false;
}
}
return true;
}
// Resolve the Cursor profile directory from either project root, profile root, or rules dir
function resolveCursorProfileDir(baseDir) {
const base = path.basename(baseDir);
// If called with .../.cursor/rules -> return .../.cursor
if (base === 'rules' && path.basename(path.dirname(baseDir)) === '.cursor') {
return path.dirname(baseDir);
}
// If called with .../.cursor -> return as-is
if (base === '.cursor') return baseDir;
// Otherwise assume project root and append .cursor
return path.join(baseDir, '.cursor');
}
// Lifecycle functions for Cursor profile
function onAddRulesProfile(targetDir, assetsDir) {
// Copy commands directory recursively
const commandsSourceDir = path.join(assetsDir, 'claude', 'commands');
const profileDir = resolveCursorProfileDir(targetDir);
const commandsDestDir = path.join(profileDir, 'commands');
if (!fs.existsSync(commandsSourceDir)) {
log(
'warn',
`[Cursor] Source commands directory does not exist: ${commandsSourceDir}`
);
return;
}
try {
// Ensure fresh state to avoid stale command files
try {
fs.rmSync(commandsDestDir, { recursive: true, force: true });
log(
'debug',
`[Cursor] Removed existing commands directory: ${commandsDestDir}`
);
} catch (deleteErr) {
// Directory might not exist, which is fine
log(
'debug',
`[Cursor] Commands directory did not exist or could not be removed: ${deleteErr.message}`
);
}
copyRecursiveSync(commandsSourceDir, commandsDestDir);
log('debug', `[Cursor] Copied commands directory to ${commandsDestDir}`);
} catch (err) {
log(
'error',
`[Cursor] An error occurred during commands copy: ${err.message}`
);
}
}
function onRemoveRulesProfile(targetDir) {
// Remove .cursor/commands directory recursively
const profileDir = resolveCursorProfileDir(targetDir);
const commandsDir = path.join(profileDir, 'commands');
if (removeDirectoryRecursive(commandsDir)) {
log(
'debug',
`[Cursor] Ensured commands directory removed at ${commandsDir}`
);
}
}
// Create and export cursor profile using the base factory
export const cursorProfile = createProfile({
@@ -8,5 +137,10 @@ export const cursorProfile = createProfile({
url: 'cursor.so',
docsUrl: 'docs.cursor.com',
targetExtension: '.mdc', // Cursor keeps .mdc extension
supportsRulesSubdirectories: true
supportsRulesSubdirectories: true,
onAdd: onAddRulesProfile,
onRemove: onRemoveRulesProfile
});
// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile };

View File

@@ -210,7 +210,7 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
if (typeof profile.onAddRulesProfile === 'function') {
try {
const assetsDir = getAssetsDir();
profile.onAddRulesProfile(projectRoot, assetsDir);
profile.onAddRulesProfile(targetDir, assetsDir);
log(
'debug',
`[Rule Transformer] Called onAddRulesProfile for ${profile.profileName}`
@@ -305,7 +305,7 @@ export function convertAllRulesToProfileRules(projectRoot, profile) {
if (typeof profile.onPostConvertRulesProfile === 'function') {
try {
const assetsDir = getAssetsDir();
profile.onPostConvertRulesProfile(projectRoot, assetsDir);
profile.onPostConvertRulesProfile(targetDir, assetsDir);
log(
'debug',
`[Rule Transformer] Called onPostConvertRulesProfile for ${profile.profileName}`
@@ -347,7 +347,7 @@ export function removeProfileRules(projectRoot, profile) {
// 1. Call onRemoveRulesProfile first (for custom cleanup like removing assets)
if (typeof profile.onRemoveRulesProfile === 'function') {
try {
profile.onRemoveRulesProfile(projectRoot);
profile.onRemoveRulesProfile(targetDir);
log(
'debug',
`[Rule Transformer] Called onRemoveRulesProfile for ${profile.profileName}`

View File

@@ -0,0 +1,52 @@
import { jest } from '@jest/globals';
// Mock AI SDK functions at the top level
jest.unstable_mockModule('ai', () => ({
generateObject: jest.fn(),
generateText: jest.fn(),
streamText: jest.fn(),
streamObject: jest.fn(),
zodSchema: jest.fn(),
JSONParseError: class JSONParseError extends Error {},
NoObjectGeneratedError: class NoObjectGeneratedError extends Error {}
}));
// Mock CLI failure scenario
jest.unstable_mockModule('ai-sdk-provider-claude-code', () => ({
createClaudeCode: jest.fn(() => {
throw new Error('Claude Code CLI not found');
})
}));
// Import the provider after mocking
const { ClaudeCodeProvider } = await import(
'../../src/ai-providers/claude-code.js'
);
describe('Claude Code Error Handling', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should throw a CLI-not-available error (with or without commandName)', () => {
const provider = new ClaudeCodeProvider();
expect(() => provider.getClient()).toThrow(
/Claude Code CLI not available/i
);
expect(() => provider.getClient({ commandName: 'test' })).toThrow(
/Claude Code CLI not available/i
);
});
it('should still support basic provider functionality', () => {
const provider = new ClaudeCodeProvider();
// These should work even if CLI is not available
expect(provider.name).toBe('Claude Code');
expect(provider.getSupportedModels()).toEqual(['sonnet', 'opus']);
expect(provider.isModelSupported('sonnet')).toBe(true);
expect(provider.isModelSupported('haiku')).toBe(false);
expect(provider.isRequiredApiKey()).toBe(false);
expect(() => provider.validateAuth()).not.toThrow();
});
});

View File

@@ -1,95 +1,128 @@
import { jest } from '@jest/globals';
// Mock the base provider to avoid circular dependencies
jest.unstable_mockModule('../../src/ai-providers/base-provider.js', () => ({
BaseAIProvider: class {
constructor() {
this.name = 'Base Provider';
}
handleError(context, error) {
throw error;
}
}
// Mock AI SDK functions at the top level
const generateText = jest.fn();
const streamText = jest.fn();
jest.unstable_mockModule('ai', () => ({
generateObject: jest.fn(),
generateText,
streamText,
streamObject: jest.fn(),
zodSchema: jest.fn(),
JSONParseError: class JSONParseError extends Error {},
NoObjectGeneratedError: class NoObjectGeneratedError extends Error {}
}));
// Mock the claude-code SDK to simulate it not being installed
jest.unstable_mockModule('@anthropic-ai/claude-code', () => {
throw new Error("Cannot find module '@anthropic-ai/claude-code'");
});
// Mock successful provider creation for all tests
const mockProvider = jest.fn((modelId) => ({
id: modelId,
doGenerate: jest.fn(),
doStream: jest.fn()
}));
mockProvider.languageModel = jest.fn((id, settings) => ({ id, settings }));
mockProvider.chat = mockProvider.languageModel;
// Import after mocking
jest.unstable_mockModule('ai-sdk-provider-claude-code', () => ({
createClaudeCode: jest.fn(() => mockProvider)
}));
// Import the provider after mocking
const { ClaudeCodeProvider } = await import(
'../../src/ai-providers/claude-code.js'
);
describe('Claude Code Optional Dependency Integration', () => {
describe('when @anthropic-ai/claude-code is not installed', () => {
it('should allow provider instantiation', () => {
// Provider should instantiate without error
const provider = new ClaudeCodeProvider();
expect(provider).toBeDefined();
expect(provider.name).toBe('Claude Code');
describe('Claude Code Integration (Optional)', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should create a working provider instance', () => {
const provider = new ClaudeCodeProvider();
expect(provider.name).toBe('Claude Code');
expect(provider.getSupportedModels()).toEqual(['sonnet', 'opus']);
});
it('should support model validation', () => {
const provider = new ClaudeCodeProvider();
expect(provider.isModelSupported('sonnet')).toBe(true);
expect(provider.isModelSupported('opus')).toBe(true);
expect(provider.isModelSupported('haiku')).toBe(false);
expect(provider.isModelSupported('unknown')).toBe(false);
});
it('should create a client successfully', () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient();
expect(client).toBeDefined();
expect(typeof client).toBe('function');
expect(client.languageModel).toBeDefined();
expect(client.chat).toBeDefined();
expect(client.chat).toBe(client.languageModel);
});
it('should pass command-specific settings to client', async () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient({ commandName: 'test-command' });
expect(client).toBeDefined();
expect(typeof client).toBe('function');
const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
expect(createClaudeCode).toHaveBeenCalledTimes(1);
});
it('should handle AI SDK generateText integration', async () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient();
// Mock successful generation
generateText.mockResolvedValueOnce({
text: 'Hello from Claude Code!',
usage: { totalTokens: 10 }
});
it('should allow client creation', () => {
const provider = new ClaudeCodeProvider();
// Client creation should work
const client = provider.getClient({});
expect(client).toBeDefined();
expect(typeof client).toBe('function');
const result = await generateText({
model: client('sonnet'),
messages: [{ role: 'user', content: 'Hello' }]
});
it('should fail with clear error when trying to use the model', async () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient({});
const model = client('opus');
// The actual usage should fail with the lazy loading error
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'Hello' }],
mode: { type: 'regular' }
})
).rejects.toThrow(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
});
it('should provide helpful error message for streaming', async () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient({});
const model = client('sonnet');
await expect(
model.doStream({
prompt: [{ role: 'user', content: 'Hello' }],
mode: { type: 'regular' }
})
).rejects.toThrow(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
expect(result.text).toBe('Hello from Claude Code!');
expect(generateText).toHaveBeenCalledWith({
model: expect.any(Object),
messages: [{ role: 'user', content: 'Hello' }]
});
});
describe('provider behavior', () => {
it('should not require API key', () => {
const provider = new ClaudeCodeProvider();
// Should not throw
expect(() => provider.validateAuth()).not.toThrow();
expect(() => provider.validateAuth({ apiKey: null })).not.toThrow();
it('should handle AI SDK streamText integration', async () => {
const provider = new ClaudeCodeProvider();
const client = provider.getClient();
// Mock successful streaming
const mockStream = {
textStream: (async function* () {
yield 'Streamed response';
})()
};
streamText.mockResolvedValueOnce(mockStream);
const streamResult = await streamText({
model: client('sonnet'),
messages: [{ role: 'user', content: 'Stream test' }]
});
it('should work with ai-services-unified when provider is configured', async () => {
// This tests that the provider can be selected but will fail appropriately
// when the actual model is used
const provider = new ClaudeCodeProvider();
expect(provider).toBeDefined();
// In real usage, ai-services-unified would:
// 1. Get the provider instance (works)
// 2. Call provider.getClient() (works)
// 3. Create a model (works)
// 4. Try to generate (fails with clear error)
expect(streamResult.textStream).toBeDefined();
expect(streamText).toHaveBeenCalledWith({
model: expect.any(Object),
messages: [{ role: 'user', content: 'Stream test' }]
});
});
it('should not require authentication validation', () => {
const provider = new ClaudeCodeProvider();
expect(provider.isRequiredApiKey()).toBe(false);
expect(() => provider.validateAuth()).not.toThrow();
expect(() => provider.validateAuth({})).not.toThrow();
expect(() => provider.validateAuth({ commandName: 'test' })).not.toThrow();
});
});

View File

@@ -372,7 +372,7 @@ describe('Complex Cross-Tag Scenarios', () => {
fs.writeFileSync(tasksPath, JSON.stringify(largeTaskSet, null, 2));
// Should complete within reasonable time
const timeout = process.env.CI ? 11000 : 6000;
const timeout = process.env.CI ? 12000 : 8000;
const startTime = Date.now();
execSync(
`node ${binPath} move --from=50 --from-tag=master --to-tag=in-progress --with-dependencies`,

View File

@@ -54,4 +54,33 @@ describe('Cursor Profile Initialization Functionality', () => {
);
expect(cursorProfile.conversionConfig.toolNames.search).toBe('search');
});
test('cursor.js has lifecycle functions for command copying', () => {
// Check that the source file contains our new lifecycle functions
expect(cursorProfileContent).toContain('function onAddRulesProfile');
expect(cursorProfileContent).toContain('function onRemoveRulesProfile');
expect(cursorProfileContent).toContain('copyRecursiveSync');
expect(cursorProfileContent).toContain('removeDirectoryRecursive');
});
test('cursor.js copies commands from claude/commands to .cursor/commands', () => {
// Check that the onAddRulesProfile function copies from the correct source
expect(cursorProfileContent).toContain(
"path.join(assetsDir, 'claude', 'commands')"
);
// Destination path is built via a resolver to handle both project root and rules dir
expect(cursorProfileContent).toContain('resolveCursorProfileDir(');
expect(cursorProfileContent).toMatch(
/path\.join\(\s*profileDir\s*,\s*['"]commands['"]\s*\)/
);
expect(cursorProfileContent).toContain(
'copyRecursiveSync(commandsSourceDir, commandsDestDir)'
);
// Check that lifecycle functions are properly registered with the profile
expect(cursorProfile.onAddRulesProfile).toBeDefined();
expect(cursorProfile.onRemoveRulesProfile).toBeDefined();
expect(typeof cursorProfile.onAddRulesProfile).toBe('function');
expect(typeof cursorProfile.onRemoveRulesProfile).toBe('function');
});
});

View File

@@ -1,21 +1,20 @@
import { jest } from '@jest/globals';
// Mock the claude-code SDK module
jest.unstable_mockModule(
'../../../src/ai-providers/custom-sdk/claude-code/index.js',
() => ({
createClaudeCode: jest.fn(() => {
const provider = (modelId, settings) => ({
// Mock language model
id: modelId,
settings
});
provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
provider.chat = provider.languageModel;
return provider;
})
// Mock the ai-sdk-provider-claude-code package
jest.unstable_mockModule('ai-sdk-provider-claude-code', () => ({
createClaudeCode: jest.fn(() => {
const provider = (modelId, settings) => ({
// Minimal mock language model surface
id: modelId,
settings,
doGenerate: jest.fn(() => ({ text: 'ok', usage: {} })),
doStream: jest.fn(() => ({ stream: true }))
});
provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
provider.chat = provider.languageModel;
return provider;
})
);
}));
// Mock the base provider
jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
@@ -74,15 +73,14 @@ describe('ClaudeCodeProvider', () => {
expect(typeof client).toBe('function');
});
it('should create client without API key or base URL', () => {
const client = provider.getClient({});
it('should create client without parameters', () => {
const client = provider.getClient();
expect(client).toBeDefined();
});
it('should handle params even though they are not used', () => {
it('should handle commandName parameter', () => {
const client = provider.getClient({
baseURL: 'https://example.com',
apiKey: 'unused-key'
commandName: 'test-command'
});
expect(client).toBeDefined();
});
@@ -95,12 +93,24 @@ describe('ClaudeCodeProvider', () => {
});
});
describe('model support', () => {
it('should return supported models', () => {
const models = provider.getSupportedModels();
expect(models).toEqual(['sonnet', 'opus']);
});
it('should check if model is supported', () => {
expect(provider.isModelSupported('sonnet')).toBe(true);
expect(provider.isModelSupported('opus')).toBe(true);
expect(provider.isModelSupported('haiku')).toBe(false);
expect(provider.isModelSupported('unknown')).toBe(false);
});
});
describe('error handling', () => {
it('should handle client initialization errors', async () => {
// Force an error by making createClaudeCode throw
const { createClaudeCode } = await import(
'../../../src/ai-providers/custom-sdk/claude-code/index.js'
);
const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
createClaudeCode.mockImplementationOnce(() => {
throw new Error('Mock initialization error');
});

View File

@@ -1,237 +0,0 @@
import { jest } from '@jest/globals';
// Mock modules before importing
jest.unstable_mockModule('@ai-sdk/provider', () => ({
NoSuchModelError: class NoSuchModelError extends Error {
constructor({ modelId, modelType }) {
super(`No such model: ${modelId}`);
this.modelId = modelId;
this.modelType = modelType;
}
}
}));
jest.unstable_mockModule('@ai-sdk/provider-utils', () => ({
generateId: jest.fn(() => 'test-id-123')
}));
jest.unstable_mockModule(
'../../../../../src/ai-providers/custom-sdk/claude-code/message-converter.js',
() => ({
convertToClaudeCodeMessages: jest.fn((prompt) => ({
messagesPrompt: 'converted-prompt',
systemPrompt: 'system'
}))
})
);
jest.unstable_mockModule(
'../../../../../src/ai-providers/custom-sdk/claude-code/json-extractor.js',
() => ({
extractJson: jest.fn((text) => text)
})
);
jest.unstable_mockModule(
'../../../../../src/ai-providers/custom-sdk/claude-code/errors.js',
() => ({
createAPICallError: jest.fn((opts) => new Error(opts.message)),
createAuthenticationError: jest.fn((opts) => new Error(opts.message))
})
);
// This mock will be controlled by tests
let mockClaudeCodeModule = null;
jest.unstable_mockModule('@anthropic-ai/claude-code', () => {
if (mockClaudeCodeModule) {
return mockClaudeCodeModule;
}
throw new Error("Cannot find module '@anthropic-ai/claude-code'");
});
// Import the module under test
const { ClaudeCodeLanguageModel } = await import(
'../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js'
);
describe('ClaudeCodeLanguageModel', () => {
beforeEach(() => {
jest.clearAllMocks();
// Reset the module mock
mockClaudeCodeModule = null;
// Clear module cache to ensure fresh imports
jest.resetModules();
});
describe('constructor', () => {
it('should initialize with valid model ID', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: { maxTurns: 5 }
});
expect(model.modelId).toBe('opus');
expect(model.settings).toEqual({ maxTurns: 5 });
expect(model.provider).toBe('claude-code');
});
it('should throw NoSuchModelError for invalid model ID', async () => {
expect(
() =>
new ClaudeCodeLanguageModel({
id: '',
settings: {}
})
).toThrow('No such model: ');
expect(
() =>
new ClaudeCodeLanguageModel({
id: null,
settings: {}
})
).toThrow('No such model: null');
});
});
describe('lazy loading of @anthropic-ai/claude-code', () => {
it('should throw error when package is not installed', async () => {
// Keep mockClaudeCodeModule as null to simulate missing package
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
})
).rejects.toThrow(
"Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider."
);
});
it('should load package successfully when available', async () => {
// Mock successful package load
const mockQuery = jest.fn(async function* () {
yield {
type: 'assistant',
message: { content: [{ type: 'text', text: 'Hello' }] }
};
yield {
type: 'result',
subtype: 'done',
usage: { output_tokens: 10, input_tokens: 5 }
};
});
mockClaudeCodeModule = {
query: mockQuery,
AbortError: class AbortError extends Error {}
};
// Need to re-import to get fresh module with mocks
jest.resetModules();
const { ClaudeCodeLanguageModel: FreshModel } = await import(
'../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js'
);
const model = new FreshModel({
id: 'opus',
settings: {}
});
const result = await model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
});
expect(result.text).toBe('Hello');
expect(mockQuery).toHaveBeenCalled();
});
it('should only attempt to load package once', async () => {
// Get a fresh import to ensure clean state
jest.resetModules();
const { ClaudeCodeLanguageModel: TestModel } = await import(
'../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js'
);
const model = new TestModel({
id: 'opus',
settings: {}
});
// First call should throw
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
})
).rejects.toThrow('Claude Code SDK is not installed');
// Second call should also throw without trying to load again
await expect(
model.doGenerate({
prompt: [{ role: 'user', content: 'test' }],
mode: { type: 'regular' }
})
).rejects.toThrow('Claude Code SDK is not installed');
});
});
describe('generateUnsupportedWarnings', () => {
it('should generate warnings for unsupported parameters', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
const warnings = model.generateUnsupportedWarnings({
temperature: 0.7,
maxTokens: 1000,
topP: 0.9,
seed: 42
});
expect(warnings).toHaveLength(4);
expect(warnings[0]).toEqual({
type: 'unsupported-setting',
setting: 'temperature',
details:
'Claude Code CLI does not support the temperature parameter. It will be ignored.'
});
});
it('should return empty array when no unsupported parameters', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
const warnings = model.generateUnsupportedWarnings({});
expect(warnings).toEqual([]);
});
});
describe('getModel', () => {
it('should map model IDs correctly', () => {
const model = new ClaudeCodeLanguageModel({
id: 'opus',
settings: {}
});
expect(model.getModel()).toBe('opus');
});
it('should return unmapped model IDs as-is', () => {
const model = new ClaudeCodeLanguageModel({
id: 'custom-model',
settings: {}
});
expect(model.getModel()).toBe('custom-model');
});
});
});

View File

@@ -524,7 +524,7 @@ describe('GeminiCliProvider', () => {
}),
system: 'You are a helpful assistant',
messages: [{ role: 'user', content: 'Hello' }],
maxTokens: 100,
maxOutputTokens: 100,
temperature: 0.7
});
expect(result.text).toBe('Hello! How can I help you?');
@@ -550,7 +550,7 @@ describe('GeminiCliProvider', () => {
}),
system: undefined,
messages: [{ role: 'user', content: 'Hello' }],
maxTokens: 100,
maxOutputTokens: 100,
temperature: 0.7
});
});
@@ -570,7 +570,7 @@ describe('GeminiCliProvider', () => {
}),
system: 'You are a helpful assistant',
messages: [{ role: 'user', content: 'Hello' }],
maxTokens: 100,
maxOutputTokens: 100,
temperature: 0.7
});
expect(result).toBe(mockStream);
@@ -609,7 +609,7 @@ describe('GeminiCliProvider', () => {
messages: [{ role: 'user', content: 'Hello' }],
schema: mockObjectParams.schema,
mode: 'json',
maxTokens: 100,
maxOutputTokens: 100,
temperature: 0.7
});
expect(result.object).toEqual({ result: 'success' });

View File

@@ -1,11 +1,10 @@
/**
* Tests for OpenAI Provider - Token parameter handling for GPT-5
* Tests for OpenAI Provider
*
* This test suite covers:
* 1. Correct identification of GPT-5 models requiring max_completion_tokens
* 2. Token parameter preparation for different model types
* 3. Validation of maxTokens parameter
* 4. Integer coercion of token values
* 1. Validation of maxTokens parameter
* 2. Client creation and configuration
* 3. Model handling
*/
import { jest } from '@jest/globals';
@@ -26,81 +25,6 @@ describe('OpenAIProvider', () => {
jest.clearAllMocks();
});
describe('requiresMaxCompletionTokens', () => {
it('should return true for GPT-5 models', () => {
expect(provider.requiresMaxCompletionTokens('gpt-5')).toBe(true);
expect(provider.requiresMaxCompletionTokens('gpt-5-mini')).toBe(true);
expect(provider.requiresMaxCompletionTokens('gpt-5-nano')).toBe(true);
expect(provider.requiresMaxCompletionTokens('gpt-5-turbo')).toBe(true);
});
it('should return false for non-GPT-5 models', () => {
expect(provider.requiresMaxCompletionTokens('gpt-4')).toBe(false);
expect(provider.requiresMaxCompletionTokens('gpt-4o')).toBe(false);
expect(provider.requiresMaxCompletionTokens('gpt-3.5-turbo')).toBe(false);
expect(provider.requiresMaxCompletionTokens('o1')).toBe(false);
expect(provider.requiresMaxCompletionTokens('o1-mini')).toBe(false);
});
it('should handle null/undefined modelId', () => {
expect(provider.requiresMaxCompletionTokens(null)).toBeFalsy();
expect(provider.requiresMaxCompletionTokens(undefined)).toBeFalsy();
expect(provider.requiresMaxCompletionTokens('')).toBeFalsy();
});
});
describe('prepareTokenParam', () => {
it('should return max_completion_tokens for GPT-5 models', () => {
const result = provider.prepareTokenParam('gpt-5', 1000);
expect(result).toEqual({ max_completion_tokens: 1000 });
});
it('should return maxTokens for non-GPT-5 models', () => {
const result = provider.prepareTokenParam('gpt-4', 1000);
expect(result).toEqual({ maxTokens: 1000 });
});
it('should coerce token value to integer', () => {
// Float values
const result1 = provider.prepareTokenParam('gpt-5', 1000.7);
expect(result1).toEqual({ max_completion_tokens: 1000 });
const result2 = provider.prepareTokenParam('gpt-4', 1000.7);
expect(result2).toEqual({ maxTokens: 1000 });
// String float
const result3 = provider.prepareTokenParam('gpt-5', '1000.7');
expect(result3).toEqual({ max_completion_tokens: 1000 });
// String integers (common CLI input path)
expect(provider.prepareTokenParam('gpt-5', '1000')).toEqual({
max_completion_tokens: 1000
});
expect(provider.prepareTokenParam('gpt-4', '1000')).toEqual({
maxTokens: 1000
});
});
it('should return empty object for undefined maxTokens', () => {
const result = provider.prepareTokenParam('gpt-5', undefined);
expect(result).toEqual({});
});
it('should handle edge cases', () => {
// Test with 0 (should still pass through as 0)
const result1 = provider.prepareTokenParam('gpt-5', 0);
expect(result1).toEqual({ max_completion_tokens: 0 });
// Test with string number
const result2 = provider.prepareTokenParam('gpt-5', '100');
expect(result2).toEqual({ max_completion_tokens: 100 });
// Test with negative number (will be floored, validation happens elsewhere)
const result3 = provider.prepareTokenParam('gpt-4', -10.5);
expect(result3).toEqual({ maxTokens: -11 });
});
});
describe('validateOptionalParams', () => {
it('should accept valid maxTokens values', () => {
expect(() =>

View File

@@ -27,10 +27,6 @@ jest.mock('chalk', () => ({
jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`));
jest.mock('@anthropic-ai/sdk', () => ({
Anthropic: jest.fn().mockImplementation(() => ({}))
}));
// Mock utils module
const mockTaskExists = jest.fn();
const mockFormatTaskId = jest.fn();

View File

@@ -8,18 +8,37 @@ jest.mock('child_process', () => ({
execSync: jest.fn()
}));
// Mock console methods
jest.mock('console', () => ({
// Mock console methods to avoid chalk issues
const mockLog = jest.fn();
const originalConsole = global.console;
const mockConsole = {
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
clear: jest.fn()
};
global.console = mockConsole;
// Mock utils logger to avoid chalk dependency issues
await jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
default: undefined,
log: mockLog,
isSilentMode: () => false
}));
// Import the cursor profile after mocking
const { cursorProfile, onAddRulesProfile, onRemoveRulesProfile } = await import(
'../../../src/profiles/cursor.js'
);
describe('Cursor Integration', () => {
let tempDir;
afterAll(() => {
global.console = originalConsole;
});
beforeEach(() => {
jest.clearAllMocks();
@@ -75,4 +94,127 @@ describe('Cursor Integration', () => {
{ recursive: true }
);
});
test('cursor profile has lifecycle functions for command copying', () => {
// Assert that the profile exports the lifecycle functions
expect(typeof onAddRulesProfile).toBe('function');
expect(typeof onRemoveRulesProfile).toBe('function');
expect(cursorProfile.onAddRulesProfile).toBe(onAddRulesProfile);
expect(cursorProfile.onRemoveRulesProfile).toBe(onRemoveRulesProfile);
});
describe('command copying lifecycle', () => {
let mockAssetsDir;
let mockTargetDir;
beforeEach(() => {
mockAssetsDir = path.join(tempDir, 'assets');
mockTargetDir = path.join(tempDir, 'target');
// Reset all mocks
jest.clearAllMocks();
// Mock fs methods for the lifecycle functions
jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
const pathStr = filePath.toString();
if (pathStr.includes('claude/commands')) {
return true; // Mock that source commands exist
}
return false;
});
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
jest.spyOn(fs, 'readdirSync').mockImplementation(() => ['tm']);
jest
.spyOn(fs, 'statSync')
.mockImplementation(() => ({ isDirectory: () => true }));
jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
jest.spyOn(fs, 'rmSync').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
test('onAddRulesProfile copies commands from assets to .cursor/commands', () => {
// Detect if cpSync exists and set up appropriate spy
if (fs.cpSync) {
const cpSpy = jest.spyOn(fs, 'cpSync').mockImplementation(() => {});
// Act
onAddRulesProfile(mockTargetDir, mockAssetsDir);
// Assert
expect(fs.existsSync).toHaveBeenCalledWith(
path.join(mockAssetsDir, 'claude', 'commands')
);
expect(cpSpy).toHaveBeenCalledWith(
path.join(mockAssetsDir, 'claude', 'commands'),
path.join(mockTargetDir, '.cursor', 'commands'),
expect.objectContaining({ recursive: true, force: true })
);
} else {
// Act
onAddRulesProfile(mockTargetDir, mockAssetsDir);
// Assert
expect(fs.existsSync).toHaveBeenCalledWith(
path.join(mockAssetsDir, 'claude', 'commands')
);
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(mockTargetDir, '.cursor', 'commands'),
{ recursive: true }
);
expect(fs.copyFileSync).toHaveBeenCalled();
}
});
test('onAddRulesProfile handles missing source directory gracefully', () => {
// Arrange - mock source directory not existing
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
// Act
onAddRulesProfile(mockTargetDir, mockAssetsDir);
// Assert - should not attempt to copy anything
expect(fs.mkdirSync).not.toHaveBeenCalled();
expect(fs.copyFileSync).not.toHaveBeenCalled();
});
test('onRemoveRulesProfile removes .cursor/commands directory', () => {
// Arrange - mock directory exists
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
// Act
onRemoveRulesProfile(mockTargetDir);
// Assert
expect(fs.rmSync).toHaveBeenCalledWith(
path.join(mockTargetDir, '.cursor', 'commands'),
{ recursive: true, force: true }
);
});
test('onRemoveRulesProfile handles missing directory gracefully', () => {
// Arrange - mock directory doesn't exist
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
// Act
onRemoveRulesProfile(mockTargetDir);
// Assert - should still return true but not attempt removal
expect(fs.rmSync).not.toHaveBeenCalled();
});
test('onRemoveRulesProfile handles removal errors gracefully', () => {
// Arrange - mock directory exists but removal fails
jest.spyOn(fs, 'existsSync').mockImplementation(() => true);
jest.spyOn(fs, 'rmSync').mockImplementation(() => {
throw new Error('Permission denied');
});
// Act & Assert - should not throw
expect(() => onRemoveRulesProfile(mockTargetDir)).not.toThrow();
});
});
});

View File

@@ -19,7 +19,13 @@
"@tm/cli": ["./apps/cli/src/index.ts"],
"@tm/cli/*": ["./apps/cli/src/*"],
"@tm/build-config": ["./packages/build-config/src/index.ts"],
"@tm/build-config/*": ["./packages/build-config/src/*"]
"@tm/build-config/*": ["./packages/build-config/src/*"],
"@tm/ai-sdk-provider-grok-cli": [
"./packages/ai-sdk-provider-grok-cli/src/index.ts"
],
"@tm/ai-sdk-provider-grok-cli/*": [
"./packages/ai-sdk-provider-grok-cli/src/*"
]
}
},
"tsx": {

View File

@@ -1,8 +1,6 @@
import { defineConfig } from 'tsdown';
import { baseConfig, mergeConfig } from '@tm/build-config';
import { load as dotenvLoad } from 'dotenv-mono';
dotenvLoad();
import 'dotenv/config';
// Get all TM_PUBLIC_* env variables for build-time injection
const getBuildTimeEnvs = () => {