mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
chore: remove obsolete Cursor CLI integration documentation
- Deleted the Cursor CLI integration analysis document, phase prompt, README, and related phase files as they are no longer relevant to the current project structure. - This cleanup helps streamline the project and remove outdated references, ensuring a more maintainable codebase.
This commit is contained in:
@@ -1,89 +0,0 @@
|
||||
# Global Prompt for Cursor CLI Integration Phases
|
||||
|
||||
Copy the prompt below when starting a new Claude session for any phase.
|
||||
|
||||
---
|
||||
|
||||
## Prompt Template
|
||||
|
||||
```
|
||||
I'm implementing the Cursor CLI integration for AutoMaker.
|
||||
|
||||
## Context
|
||||
- Plan location: `P:\automaker\plan\cursor-cli-integration\`
|
||||
- Read the README.md first for architecture overview and design decisions
|
||||
- Then read the specific phase file I mention below
|
||||
|
||||
## Phase to Implement
|
||||
[REPLACE THIS LINE WITH: Phase X - phases/phase-X-*.md]
|
||||
|
||||
## Critical Requirements
|
||||
|
||||
### 1. Use @automaker/* Packages (see docs\llm-shared-packages.md)
|
||||
|
||||
**From @automaker/types:**
|
||||
- Reuse `InstallationStatus` (don't create new status types)
|
||||
- Use `ModelProvider` type ('claude' | 'cursor')
|
||||
- Use `CursorModelId`, `CURSOR_MODEL_MAP` for Cursor models
|
||||
|
||||
**From @automaker/utils:**
|
||||
import { createLogger, isAbortError, mkdirSafe, existsSafe } from '@automaker/utils';
|
||||
|
||||
**From @automaker/platform:**
|
||||
import { spawnJSONLProcess, getAutomakerDir } from '@automaker/platform';
|
||||
|
||||
### 2. UI Components (apps/ui)
|
||||
All UI must use components from `@/components/ui/*`:
|
||||
- Card, CardHeader, CardTitle, CardContent, CardFooter
|
||||
- Button, Badge, Label, Input, Textarea
|
||||
- Select, SelectContent, SelectItem, SelectTrigger, SelectValue
|
||||
- Checkbox, Alert, AlertDescription
|
||||
- Tabs, TabsList, TabsTrigger, TabsContent
|
||||
|
||||
Icons from `lucide-react`: Terminal (Cursor), Bot (Claude), CheckCircle2, XCircle, Loader2, RefreshCw, ExternalLink
|
||||
|
||||
### 3. API Requests (apps/ui)
|
||||
Use HttpApiClient, NOT raw fetch():
|
||||
import { api } from '@/lib/http-api-client';
|
||||
const result = await api.setup.getCursorStatus();
|
||||
|
||||
### 4. Do NOT Extend @automaker/model-resolver
|
||||
Cursor models use `CURSOR_MODEL_MAP` in @automaker/types instead.
|
||||
|
||||
## Instructions
|
||||
1. Read the phase file completely
|
||||
2. Implement each task in order
|
||||
3. Run the verification steps before marking complete
|
||||
4. Update the phase status in the markdown file when done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Phase Order
|
||||
|
||||
| Phase | File | Description |
|
||||
| ----- | -------------------------------- | ------------------------------- |
|
||||
| 0 | `phases/phase-0-analysis.md` | Analysis & Documentation |
|
||||
| 1 | `phases/phase-1-types.md` | Core Types & Configuration |
|
||||
| 2 | `phases/phase-2-provider.md` | Cursor Provider Implementation |
|
||||
| 3 | `phases/phase-3-factory.md` | Provider Factory Integration |
|
||||
| 4 | `phases/phase-4-routes.md` | Setup Routes & Status Endpoints |
|
||||
| 5 | `phases/phase-5-log-parser.md` | Log Parser Integration |
|
||||
| 6 | `phases/phase-6-setup-wizard.md` | UI Setup Wizard |
|
||||
| 7 | `phases/phase-7-settings.md` | Settings View Provider Tabs |
|
||||
| 8 | `phases/phase-8-profiles.md` | AI Profiles Integration |
|
||||
| 9 | `phases/phase-9-execution.md` | Task Execution Integration |
|
||||
| 10 | `phases/phase-10-testing.md` | Testing & Validation |
|
||||
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
Phase 0 → Phase 1 → Phase 2 → Phase 3 → Phase 4 → Phase 6
|
||||
↘ ↘ Phase 7
|
||||
Phase 5 → Phase 8 → Phase 9 → Phase 10
|
||||
```
|
||||
|
||||
Phases 4-7 can run in parallel after Phase 3.
|
||||
Phase 8 depends on Phase 1 and Phase 7.
|
||||
Phase 9 depends on Phase 8.
|
||||
Phase 10 is final integration testing.
|
||||
@@ -1,396 +0,0 @@
|
||||
# Cursor CLI Integration Plan
|
||||
|
||||
> Integration of Cursor Agent CLI (`cursor-agent`) as an alternative AI provider in AutoMaker
|
||||
|
||||
## Status Overview
|
||||
|
||||
| Phase | Name | Status | Test Status |
|
||||
| ----- | ------------------------------------------------------------ | ----------- | ----------- |
|
||||
| 0 | [Analysis & Documentation](phases/phase-0-analysis.md) | `completed` | ✅ |
|
||||
| 1 | [Core Types & Configuration](phases/phase-1-types.md) | `completed` | ✅ |
|
||||
| 2 | [Cursor Provider Implementation](phases/phase-2-provider.md) | `completed` | ✅ |
|
||||
| 3 | [Provider Factory Integration](phases/phase-3-factory.md) | `completed` | ✅ |
|
||||
| 4 | [Setup Routes & Status Endpoints](phases/phase-4-routes.md) | `completed` | ✅ |
|
||||
| 5 | [Log Parser Integration](phases/phase-5-log-parser.md) | `completed` | ✅ |
|
||||
| 6 | [UI Setup Wizard](phases/phase-6-setup-wizard.md) | `completed` | ✅ |
|
||||
| 7 | [Settings View Provider Tabs](phases/phase-7-settings.md) | `completed` | ✅ |
|
||||
| 8 | [AI Profiles Integration](phases/phase-8-profiles.md) | `completed` | ✅ |
|
||||
| 9 | [Task Execution Integration](phases/phase-9-execution.md) | `completed` | ✅ |
|
||||
| 10 | [Testing & Validation](phases/phase-10-testing.md) | `pending` | - |
|
||||
|
||||
**Status Legend:** `pending` | `in_progress` | `completed` | `blocked`
|
||||
|
||||
---
|
||||
|
||||
## Quick Links
|
||||
|
||||
- **Reference PR**: [#279](https://github.com/AutoMaker-Org/automaker/pull/279) (incomplete, patterns only)
|
||||
- **Cursor CLI Docs**: [cursor.com/docs/cli](https://cursor.com/docs/cli)
|
||||
- **Output Format Spec**: [Output Format Reference](https://cursor.com/docs/cli/reference/output-format)
|
||||
|
||||
---
|
||||
|
||||
## Architecture Summary
|
||||
|
||||
### Existing Provider Pattern
|
||||
|
||||
AutoMaker uses an extensible provider architecture:
|
||||
|
||||
```
|
||||
BaseProvider (abstract)
|
||||
├── getName(): string
|
||||
├── executeQuery(options): AsyncGenerator<ProviderMessage>
|
||||
├── detectInstallation(): Promise<InstallationStatus>
|
||||
└── getAvailableModels(): ModelDefinition[]
|
||||
|
||||
ClaudeProvider extends BaseProvider
|
||||
└── Uses @anthropic-ai/claude-agent-sdk
|
||||
|
||||
ProviderFactory
|
||||
└── getProviderForModel(modelId) → routes to correct provider
|
||||
```
|
||||
|
||||
### Target Architecture
|
||||
|
||||
```
|
||||
BaseProvider
|
||||
├── ClaudeProvider (existing)
|
||||
└── CursorProvider (new)
|
||||
├── Spawns cursor-agent CLI process
|
||||
├── Uses --output-format stream-json
|
||||
└── Normalizes events to ProviderMessage format
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Requirements
|
||||
|
||||
1. **Model Selection**: Explicit model selection via config (not just "auto" mode)
|
||||
2. **Authentication**: Browser login (`cursor-agent login`) as primary method
|
||||
3. **Setup Wizard**: Optional CLI status check (skippable, configure later)
|
||||
4. **AI Profiles**: Separate Cursor profiles with embedded thinking mode (e.g., `claude-sonnet-4-thinking`)
|
||||
5. **Settings View**: Separate tabs/sections per provider
|
||||
6. **Streaming**: Full `stream-json` parsing with tool call events for log-viewer
|
||||
7. **Error Handling**: Detailed error mapping with recovery suggestions
|
||||
|
||||
---
|
||||
|
||||
## Cursor CLI Reference
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
curl https://cursor.com/install -fsS | bash
|
||||
```
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
1. **Browser Login** (Recommended): `cursor-agent login`
|
||||
2. **API Key**: `CURSOR_API_KEY` environment variable
|
||||
|
||||
### CLI Flags for Integration
|
||||
|
||||
```bash
|
||||
cursor-agent \
|
||||
-p "prompt" # Print/non-interactive mode
|
||||
--model gpt-4o # Explicit model selection
|
||||
--output-format stream-json # NDJSON streaming
|
||||
--stream-partial-output # Real-time character streaming
|
||||
--force # Allow file modifications
|
||||
```
|
||||
|
||||
### Available Models (from Cursor docs)
|
||||
|
||||
| Model ID | Description | Thinking |
|
||||
| -------------------------- | -------------------------- | -------- |
|
||||
| `auto` | Auto-select best model | - |
|
||||
| `claude-sonnet-4` | Claude Sonnet 4 | No |
|
||||
| `claude-sonnet-4-thinking` | Claude Sonnet 4 + Thinking | Yes |
|
||||
| `gpt-4o` | GPT-4o | No |
|
||||
| `gpt-4o-mini` | GPT-4o Mini | No |
|
||||
| `gemini-2.5-pro` | Gemini 2.5 Pro | No |
|
||||
| `o3-mini` | O3 Mini (reasoning) | Built-in |
|
||||
|
||||
---
|
||||
|
||||
## Stream JSON Event Types
|
||||
|
||||
### System Init
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "system",
|
||||
"subtype": "init",
|
||||
"apiKeySource": "login",
|
||||
"cwd": "/path",
|
||||
"session_id": "uuid",
|
||||
"model": "Claude 4 Sonnet",
|
||||
"permissionMode": "default"
|
||||
}
|
||||
```
|
||||
|
||||
### User Message
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "user",
|
||||
"message": { "role": "user", "content": [{ "type": "text", "text": "prompt" }] },
|
||||
"session_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
### Assistant Message
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "assistant",
|
||||
"message": { "role": "assistant", "content": [{ "type": "text", "text": "response" }] },
|
||||
"session_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Call Started
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool_call",
|
||||
"subtype": "started",
|
||||
"call_id": "id",
|
||||
"tool_call": { "readToolCall": { "args": { "path": "file.txt" } } },
|
||||
"session_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
### Tool Call Completed
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool_call",
|
||||
"subtype": "completed",
|
||||
"call_id": "id",
|
||||
"tool_call": {
|
||||
"readToolCall": {
|
||||
"args": { "path": "file.txt" },
|
||||
"result": { "success": { "content": "...", "totalLines": 54 } }
|
||||
}
|
||||
},
|
||||
"session_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
### Result (Final)
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "result",
|
||||
"subtype": "success",
|
||||
"duration_ms": 1234,
|
||||
"is_error": false,
|
||||
"result": "full text",
|
||||
"session_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
### Files to Create
|
||||
|
||||
| File | Phase | Description |
|
||||
| ------------------------------------------------------------------------------ | ----- | ---------------------------- |
|
||||
| `libs/types/src/cursor-models.ts` | 1 | Cursor model definitions |
|
||||
| `apps/server/src/providers/cursor-provider.ts` | 2 | Main provider implementation |
|
||||
| `apps/server/src/providers/cursor-config-manager.ts` | 2 | Config file management |
|
||||
| `apps/server/src/routes/setup/routes/cursor-status.ts` | 4 | CLI status endpoint |
|
||||
| `apps/server/src/routes/setup/routes/cursor-config.ts` | 4 | Config management endpoints |
|
||||
| `apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx` | 6 | Setup wizard step |
|
||||
| `apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx` | 7 | Settings tab |
|
||||
| `apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx` | 7 | Tab container |
|
||||
| `apps/server/tests/unit/providers/cursor-provider.test.ts` | 10 | Unit tests |
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Phase | Changes |
|
||||
| ------------------------------------------------------------------------ | ----- | ------------------------------ |
|
||||
| `libs/types/src/index.ts` | 1 | Export Cursor types |
|
||||
| `libs/types/src/settings.ts` | 1 | Extend `ModelProvider` type |
|
||||
| `apps/server/src/providers/provider-factory.ts` | 3 | Add Cursor routing |
|
||||
| `apps/server/src/routes/setup/index.ts` | 4 | Register Cursor routes |
|
||||
| `apps/ui/src/lib/log-parser.ts` | 5 | Add Cursor event normalization |
|
||||
| `apps/ui/src/components/views/setup-view.tsx` | 6 | Add Cursor setup step |
|
||||
| `apps/ui/src/components/views/profiles-view/components/profile-form.tsx` | 8 | Add Cursor provider fields |
|
||||
| `apps/server/src/services/agent-service.ts` | 9 | Use ProviderFactory |
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Between Phases
|
||||
|
||||
```
|
||||
Phase 0 ─────────────────────────────────────────────┐
|
||||
│ │
|
||||
Phase 1 (Types) ─────────────────────────────────────┤
|
||||
│ │
|
||||
Phase 2 (Provider) ──────────────────────────────────┤
|
||||
│ │
|
||||
Phase 3 (Factory) ───────────────────────────────────┤
|
||||
│ │
|
||||
├── Phase 4 (Routes) ────────────────────────────┤
|
||||
│ │ │
|
||||
│ ├── Phase 6 (Setup Wizard) ──────────────┤
|
||||
│ │ │
|
||||
│ └── Phase 7 (Settings View) ─────────────┤
|
||||
│ │
|
||||
├── Phase 5 (Log Parser) ────────────────────────┤
|
||||
│ │
|
||||
└── Phase 8 (Profiles) ──────────────────────────┤
|
||||
│ │
|
||||
Phase 9 (Execution) ─────────────────────┤
|
||||
│
|
||||
Phase 10 (Tests) ┘
|
||||
```
|
||||
|
||||
### External Dependencies
|
||||
|
||||
- `cursor-agent` CLI must be installed for testing
|
||||
- Cursor account for authentication testing
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### 1. Use HttpApiClient for All API Requests
|
||||
|
||||
All UI components must use `HttpApiClient` from `@/lib/http-api-client.ts` instead of raw `fetch()`:
|
||||
|
||||
```typescript
|
||||
// ✓ Correct - uses HttpApiClient
|
||||
import { api } from '@/lib/http-api-client';
|
||||
const result = await api.setup.getCursorStatus();
|
||||
|
||||
// ✗ Incorrect - raw fetch
|
||||
const response = await fetch('/api/setup/cursor-status');
|
||||
```
|
||||
|
||||
New Cursor API methods added to `HttpApiClient.setup`:
|
||||
|
||||
- `getCursorStatus()` - Installation and auth status
|
||||
- `getCursorConfig()` - Configuration settings
|
||||
- `setCursorDefaultModel(model)` - Update default model
|
||||
- `setCursorModels(models)` - Update enabled models
|
||||
|
||||
### 2. Use Existing UI Components
|
||||
|
||||
All UI must use components from `@/components/ui/*`:
|
||||
|
||||
- `Card`, `CardHeader`, `CardTitle`, `CardContent` - Layout
|
||||
- `Button`, `Badge`, `Label` - Controls
|
||||
- `Select`, `SelectContent`, `SelectItem`, `SelectTrigger`, `SelectValue` - Selection
|
||||
- `Checkbox` - Toggle inputs
|
||||
- `Alert`, `AlertDescription` - Messages
|
||||
- `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` - Navigation
|
||||
|
||||
Icons from `lucide-react`:
|
||||
|
||||
- `Terminal` - Cursor provider
|
||||
- `Bot` - Claude provider
|
||||
- `CheckCircle2`, `XCircle` - Status indicators
|
||||
- `Loader2` - Loading states
|
||||
- `RefreshCw` - Refresh action
|
||||
- `ExternalLink` - External links
|
||||
|
||||
### 3. Cursor CLI Installation Paths
|
||||
|
||||
Based on official cursor-agent install script:
|
||||
|
||||
**Linux/macOS:**
|
||||
|
||||
- Primary symlink: `~/.local/bin/cursor-agent`
|
||||
- Versions directory: `~/.local/share/cursor-agent/versions/<version>/cursor-agent`
|
||||
- Fallback: `/usr/local/bin/cursor-agent`
|
||||
|
||||
**Windows:**
|
||||
|
||||
- Primary: `%APPDATA%\Local\Programs\cursor-agent\cursor-agent.exe`
|
||||
- Fallback: `~/.local/bin/cursor-agent.exe`
|
||||
- Fallback: `C:\Program Files\cursor-agent\cursor-agent.exe`
|
||||
|
||||
### 4. Use @automaker/\* Packages
|
||||
|
||||
All server-side code must use shared packages from `libs/`:
|
||||
|
||||
**From `@automaker/types`:**
|
||||
|
||||
- Reuse existing `InstallationStatus` (don't create `CursorInstallationStatus`)
|
||||
- Extend `ModelProvider` type to include `'cursor'`
|
||||
- Extend `DEFAULT_MODELS` to include `cursor: 'auto'`
|
||||
- Update `ModelOption.provider` from `'claude'` to `ModelProvider`
|
||||
|
||||
**From `@automaker/utils`:**
|
||||
|
||||
```typescript
|
||||
import { createLogger, isAbortError } from '@automaker/utils';
|
||||
|
||||
const logger = createLogger('CursorProvider');
|
||||
// Use isAbortError() for abort signal detection
|
||||
```
|
||||
|
||||
**From `@automaker/platform`:**
|
||||
|
||||
```typescript
|
||||
import { spawnJSONLProcess, getAutomakerDir } from '@automaker/platform';
|
||||
|
||||
// Use spawnJSONLProcess for JSONL streaming (handles buffering, timeout, abort)
|
||||
// Use getAutomakerDir for consistent .automaker path resolution
|
||||
```
|
||||
|
||||
### 5. Do NOT Extend @automaker/model-resolver
|
||||
|
||||
The model-resolver is Claude-specific and should **not** be extended for Cursor:
|
||||
|
||||
- Claude uses aliases (`sonnet` → `claude-sonnet-4-5-20250929`)
|
||||
- Cursor model IDs are final-form (`claude-sonnet-4` passed directly to CLI)
|
||||
- Cursor models have metadata (`hasThinking`, `tier`) that doesn't fit the string-only map
|
||||
|
||||
Cursor models use their own `CURSOR_MODEL_MAP` in `@automaker/types`.
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
1. **Phase Isolation**: Each phase can be tested independently
|
||||
2. **Feature Flags**: Cursor provider can be disabled if issues arise
|
||||
3. **Fallback**: Default to Claude provider for unknown models
|
||||
4. **Graceful Degradation**: UI shows "not installed" state clearly
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Plan
|
||||
|
||||
1. **Start with Phase 0** - Read and understand existing patterns
|
||||
2. **Complete phases sequentially** - Dependencies require order
|
||||
3. **Test each phase** - Run the verification steps before moving on
|
||||
4. **Update status** - Mark phases as `in_progress`, `completed`, or `blocked`
|
||||
5. **Document issues** - Add notes to individual phase files
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Phase | Change |
|
||||
| ---------- | ----- | ---------------------------------------------------------------------------------- |
|
||||
| 2025-12-27 | - | Initial plan created |
|
||||
| 2025-12-27 | 2 | Updated findCliPath() with platform-specific paths and versions directory scanning |
|
||||
| 2025-12-27 | 4 | Updated to use HttpApiClient instead of raw fetch |
|
||||
| 2025-12-27 | 6 | Updated to use HttpApiClient and existing UI components |
|
||||
| 2025-12-27 | 7 | Updated to use HttpApiClient and existing UI components |
|
||||
| 2025-12-27 | - | Added Design Decisions section to README |
|
||||
| 2025-12-27 | 2 | Updated to use `createLogger` from `@automaker/utils` |
|
||||
| 2025-12-27 | 4 | Updated to use `createLogger` from `@automaker/utils` |
|
||||
| 2025-12-27 | 8 | Added proper UI component imports from `@/components/ui/*` |
|
||||
| 2025-12-27 | 1 | Added tasks 1.5-1.7: ModelOption, DEFAULT_MODELS, reuse InstallationStatus |
|
||||
| 2025-12-27 | 2 | Refactored to use `spawnJSONLProcess` and `isAbortError` from @automaker packages |
|
||||
| 2025-12-27 | - | Added design decisions 4-5: @automaker packages usage, model-resolver note |
|
||||
| 2025-12-28 | 9 | Completed: ModelSelector with Cursor models, provider tracking in execution events |
|
||||
@@ -1,392 +0,0 @@
|
||||
# Cursor CLI Integration Analysis
|
||||
|
||||
> Phase 0 Analysis Document for AutoMaker Cursor Integration
|
||||
> Generated: 2025-12-28
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document analyzes the existing Claude CLI integration architecture in AutoMaker and documents the Cursor CLI (cursor-agent) behavior to plan a parallel provider implementation.
|
||||
|
||||
## 1. Current Architecture Analysis
|
||||
|
||||
### 1.1 Provider System (`apps/server/src/providers/`)
|
||||
|
||||
#### BaseProvider (`base-provider.ts`)
|
||||
|
||||
- Abstract class with core interface for all providers
|
||||
- Key methods:
|
||||
- `execute(options)` - Runs the CLI process
|
||||
- `buildCommand(options)` - Constructs CLI command
|
||||
- `parseOutput(output)` - Parses CLI response
|
||||
- `buildSystemPrompt(options)` - Constructs system prompts
|
||||
- Uses `@automaker/platform.spawnJSONLProcess()` for CLI execution
|
||||
- Handles abort signals for cancellation
|
||||
|
||||
#### ClaudeProvider (`claude-provider.ts`)
|
||||
|
||||
- Extends BaseProvider for Claude CLI
|
||||
- Uses Claude Agent SDK via `@anthropic-ai/claude-code` package
|
||||
- Key features:
|
||||
- Thinking levels (none, low, medium, high, ultrathink)
|
||||
- Model selection (haiku, sonnet, opus)
|
||||
- System prompt injection
|
||||
- Session/conversation management
|
||||
- MCP server support
|
||||
- SDK options built via `buildSdkOptions()` in `sdk-options.ts`
|
||||
|
||||
#### ProviderFactory (`provider-factory.ts`)
|
||||
|
||||
- Creates provider instances based on type
|
||||
- Currently only supports 'claude' provider
|
||||
- Extension point for adding new providers
|
||||
|
||||
#### Types (`types.ts`)
|
||||
|
||||
- `ProviderType`: Currently `'claude'`
|
||||
- `ProviderConfig`: Configuration for providers
|
||||
- `ExecuteResult`: Standardized result format
|
||||
|
||||
### 1.2 Service Integration
|
||||
|
||||
#### AgentService (`agent-service.ts`)
|
||||
|
||||
- Manages chat agent sessions
|
||||
- Uses ClaudeProvider for execution
|
||||
- Handles streaming output to clients
|
||||
- Session persistence and history
|
||||
|
||||
#### AutoModeService (`auto-mode-service.ts`)
|
||||
|
||||
- Orchestrates feature generation workflow
|
||||
- Manages concurrent feature execution
|
||||
- Uses provider for each feature task
|
||||
- Handles planning, execution, verification phases
|
||||
|
||||
#### SDK Options (`sdk-options.ts`)
|
||||
|
||||
- Builds Claude SDK options from settings
|
||||
- Handles thinking level configuration
|
||||
- Maps model aliases to full model IDs
|
||||
- Configures MCP servers
|
||||
|
||||
### 1.3 UI Components
|
||||
|
||||
#### LogParser (`log-parser.ts`)
|
||||
|
||||
- Parses agent output into structured entries
|
||||
- Detects entry types: tool_call, tool_result, phase, error, success, etc.
|
||||
- Extracts metadata: tool name, file path, summary
|
||||
- Claude-specific patterns (🔧 Tool:, etc.)
|
||||
|
||||
#### LogViewer (`log-viewer.tsx`)
|
||||
|
||||
- Renders parsed log entries
|
||||
- Collapsible sections
|
||||
- Filtering by type/category
|
||||
- Tool-specific icons and colors
|
||||
|
||||
### 1.4 Setup Flow
|
||||
|
||||
#### Claude Status Detection (`get-claude-status.ts`)
|
||||
|
||||
- Checks CLI installation via `which`/`where`
|
||||
- Searches common installation paths
|
||||
- Detects authentication via:
|
||||
- `~/.claude/stats-cache.json` (activity)
|
||||
- `~/.claude/.credentials.json` (OAuth)
|
||||
- Environment variables (`ANTHROPIC_API_KEY`)
|
||||
- Stored API keys in AutoMaker
|
||||
|
||||
#### Setup View (`setup-view.tsx`)
|
||||
|
||||
- Multi-step wizard: welcome → theme → claude → github → complete
|
||||
- `ClaudeSetupStep` handles CLI detection and auth
|
||||
- Skip option for users without Claude
|
||||
|
||||
#### HTTP API Client (`http-api-client.ts`)
|
||||
|
||||
- Client-side API wrapper
|
||||
- `setup.getClaudeStatus()` - Get Claude CLI status
|
||||
- `setup.installClaude()` - Trigger installation
|
||||
- `setup.authClaude()` - Trigger authentication
|
||||
|
||||
### 1.5 Types Package (`@automaker/types`)
|
||||
|
||||
#### Model Types (`model.ts`)
|
||||
|
||||
```typescript
|
||||
CLAUDE_MODEL_MAP = {
|
||||
haiku: 'claude-haiku-4-5-20251001',
|
||||
sonnet: 'claude-sonnet-4-5-20250929',
|
||||
opus: 'claude-opus-4-5-20251101',
|
||||
};
|
||||
```
|
||||
|
||||
#### Settings Types (`settings.ts`)
|
||||
|
||||
- `ModelProvider`: Currently `'claude'` only
|
||||
- `AIProfile`: Provider field for profiles
|
||||
- `ThinkingLevel`: Reasoning intensity levels
|
||||
|
||||
#### Model Display (`model-display.ts`)
|
||||
|
||||
- `CLAUDE_MODELS`: UI metadata for model selection
|
||||
- `getModelDisplayName()`: Human-readable names
|
||||
|
||||
## 2. Cursor CLI Behavior Analysis
|
||||
|
||||
### 2.1 Installation & Location
|
||||
|
||||
- **Binary**: `cursor-agent`
|
||||
- **Installed at**: `~/.local/bin/cursor-agent`
|
||||
- **Version tested**: `2025.12.17-996666f`
|
||||
- **Config directory**: `~/.cursor/`
|
||||
- **Config file**: `~/.cursor/cli-config.json`
|
||||
|
||||
### 2.2 Authentication
|
||||
|
||||
```bash
|
||||
# Login command
|
||||
cursor-agent login
|
||||
|
||||
# Status check
|
||||
cursor-agent status # or `cursor-agent whoami`
|
||||
|
||||
# Logout
|
||||
cursor-agent logout
|
||||
```
|
||||
|
||||
Authentication is browser-based (OAuth) by default. Status output:
|
||||
|
||||
```
|
||||
✓ Logged in as user@example.com
|
||||
```
|
||||
|
||||
### 2.3 CLI Options
|
||||
|
||||
| Option | Description |
|
||||
| -------------------------- | ------------------------------------------- |
|
||||
| `--print` | Non-interactive mode, outputs to stdout |
|
||||
| `--output-format <format>` | `text`, `json`, or `stream-json` |
|
||||
| `--model <model>` | Model selection (e.g., `gpt-5`, `sonnet-4`) |
|
||||
| `--workspace <path>` | Working directory |
|
||||
| `--resume [chatId]` | Resume previous session |
|
||||
| `--api-key <key>` | API key (or `CURSOR_API_KEY` env) |
|
||||
| `--force` | Auto-approve tool calls |
|
||||
| `--approve-mcps` | Auto-approve MCP servers |
|
||||
|
||||
### 2.4 Output Formats
|
||||
|
||||
#### JSON Format (`--output-format json`)
|
||||
|
||||
Single JSON object on completion:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "result",
|
||||
"subtype": "success",
|
||||
"is_error": false,
|
||||
"duration_ms": 2691,
|
||||
"result": "Response text here",
|
||||
"session_id": "uuid",
|
||||
"request_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
#### Stream-JSON Format (`--output-format stream-json`)
|
||||
|
||||
JSONL (one JSON per line) during execution:
|
||||
|
||||
1. **Init event**:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "system",
|
||||
"subtype": "init",
|
||||
"apiKeySource": "login",
|
||||
"cwd": "/path",
|
||||
"session_id": "uuid",
|
||||
"model": "Composer 1"
|
||||
}
|
||||
```
|
||||
|
||||
2. **User message**:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "user",
|
||||
"message": { "role": "user", "content": [{ "type": "text", "text": "prompt" }] },
|
||||
"session_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Thinking events**:
|
||||
|
||||
```json
|
||||
{"type":"thinking","subtype":"delta","text":"","session_id":"uuid","timestamp_ms":123}
|
||||
{"type":"thinking","subtype":"completed","session_id":"uuid","timestamp_ms":123}
|
||||
```
|
||||
|
||||
4. **Assistant response**:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "assistant",
|
||||
"message": { "role": "assistant", "content": [{ "type": "text", "text": "response" }] },
|
||||
"session_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
5. **Result**:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "result",
|
||||
"subtype": "success",
|
||||
"duration_ms": 1825,
|
||||
"is_error": false,
|
||||
"result": "response",
|
||||
"session_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 Config File Structure
|
||||
|
||||
`~/.cursor/cli-config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["Shell(ls)"],
|
||||
"deny": []
|
||||
},
|
||||
"version": 1,
|
||||
"model": {
|
||||
"modelId": "composer-1",
|
||||
"displayModelId": "composer-1",
|
||||
"displayName": "Composer 1"
|
||||
},
|
||||
"approvalMode": "allowlist",
|
||||
"sandbox": {
|
||||
"mode": "disabled",
|
||||
"networkAccess": "allowlist"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 Available Models
|
||||
|
||||
From `--help`:
|
||||
|
||||
- `gpt-5`
|
||||
- `sonnet-4`
|
||||
- `sonnet-4-thinking`
|
||||
- `composer-1` (default)
|
||||
|
||||
### 2.7 MCP Support
|
||||
|
||||
```bash
|
||||
cursor-agent mcp list # List configured servers
|
||||
cursor-agent mcp login <id> # Authenticate with server
|
||||
cursor-agent mcp list-tools <id> # List tools for server
|
||||
cursor-agent mcp disable <id> # Remove from approved list
|
||||
```
|
||||
|
||||
## 3. Integration Strategy
|
||||
|
||||
### 3.1 Types to Add (`@automaker/types`)
|
||||
|
||||
```typescript
|
||||
// settings.ts
|
||||
export type ModelProvider = 'claude' | 'cursor';
|
||||
|
||||
// model.ts (new: cursor-models.ts)
|
||||
export type CursorModelId = 'composer-1' | 'gpt-5' | 'sonnet-4' | 'sonnet-4-thinking';
|
||||
|
||||
export const CURSOR_MODEL_MAP: Record<string, CursorModelId> = {
|
||||
composer: 'composer-1',
|
||||
gpt5: 'gpt-5',
|
||||
sonnet: 'sonnet-4',
|
||||
'sonnet-thinking': 'sonnet-4-thinking',
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 Provider Implementation
|
||||
|
||||
New `CursorProvider` class extending `BaseProvider`:
|
||||
|
||||
- Override `buildCommand()` for cursor-agent CLI
|
||||
- Override `parseOutput()` for Cursor's JSONL format
|
||||
- Handle stream-json output parsing
|
||||
- Map thinking events to existing log format
|
||||
|
||||
### 3.3 Setup Flow Changes
|
||||
|
||||
1. Add `CursorSetupStep` component
|
||||
2. Add `get-cursor-status.ts` route handler
|
||||
3. Update setup wizard to detect/choose providers
|
||||
4. Add Cursor authentication flow
|
||||
|
||||
### 3.4 UI Updates
|
||||
|
||||
1. Extend `LogParser` for Cursor event types
|
||||
2. Add Cursor icon (Terminal from lucide-react)
|
||||
3. Update model selectors for Cursor models
|
||||
4. Provider toggle in settings
|
||||
|
||||
## 4. Key Differences: Claude vs Cursor
|
||||
|
||||
| Aspect | Claude CLI | Cursor CLI |
|
||||
| ---------- | --------------------------- | ----------------------- |
|
||||
| Binary | `claude` | `cursor-agent` |
|
||||
| SDK | `@anthropic-ai/claude-code` | Direct CLI spawn |
|
||||
| Config dir | `~/.claude/` | `~/.cursor/` |
|
||||
| Auth file | `.credentials.json` | `cli-config.json` |
|
||||
| Output | SDK events | JSONL stream |
|
||||
| Thinking | Extended thinking API | `thinking` events |
|
||||
| Models | haiku/sonnet/opus | composer/gpt-5/sonnet-4 |
|
||||
| Session | Conversation system | `session_id` in output |
|
||||
|
||||
## 5. Files to Create/Modify
|
||||
|
||||
### Phase 1: Types
|
||||
|
||||
- [ ] `libs/types/src/cursor-models.ts` - Cursor model definitions
|
||||
- [ ] `libs/types/src/settings.ts` - Update ModelProvider type
|
||||
- [ ] `libs/types/src/index.ts` - Export new types
|
||||
|
||||
### Phase 2: Provider
|
||||
|
||||
- [ ] `apps/server/src/providers/cursor-provider.ts` - New provider
|
||||
- [ ] `apps/server/src/providers/provider-factory.ts` - Add cursor
|
||||
- [ ] `apps/server/src/providers/types.ts` - Update ProviderType
|
||||
|
||||
### Phase 3: Setup
|
||||
|
||||
- [ ] `apps/server/src/routes/setup/get-cursor-status.ts` - Detection
|
||||
- [ ] `apps/server/src/routes/setup/routes/cursor-status.ts` - Route
|
||||
- [ ] `apps/server/src/routes/setup/index.ts` - Register route
|
||||
|
||||
### Phase 4: UI
|
||||
|
||||
- [ ] `apps/ui/src/components/views/setup-view/steps/CursorSetupStep.tsx`
|
||||
- [ ] `apps/ui/src/lib/log-parser.ts` - Cursor event parsing
|
||||
- [ ] `apps/ui/src/lib/http-api-client.ts` - Add cursor endpoints
|
||||
|
||||
## 6. Verification Checklist
|
||||
|
||||
- [x] Read all core provider files
|
||||
- [x] Read service integration files
|
||||
- [x] Read UI streaming/logging files
|
||||
- [x] Read setup flow files
|
||||
- [x] Read types package files
|
||||
- [x] Document Cursor CLI behavior
|
||||
- [x] Create this analysis document
|
||||
|
||||
## 7. Next Steps
|
||||
|
||||
Proceed to **Phase 1: Types & Interfaces** to:
|
||||
|
||||
1. Add Cursor model types to `@automaker/types`
|
||||
2. Update `ModelProvider` type
|
||||
3. Create cursor model display constants
|
||||
@@ -1,201 +0,0 @@
|
||||
# Phase 0: Analysis & Documentation
|
||||
|
||||
**Status:** `complete`
|
||||
**Dependencies:** None
|
||||
**Estimated Effort:** Research only (no code changes)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Understand existing AutoMaker architecture patterns before writing any code. Document findings to ensure consistent implementation.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 0.1: Read Core Provider Files
|
||||
|
||||
**Status:** `complete`
|
||||
|
||||
Read and understand these files:
|
||||
|
||||
| File | Purpose | Key Patterns |
|
||||
| ----------------------------------------------- | ------------------------ | --------------------------------------- |
|
||||
| `apps/server/src/providers/base-provider.ts` | Abstract base class | `executeQuery()` AsyncGenerator pattern |
|
||||
| `apps/server/src/providers/claude-provider.ts` | Reference implementation | SDK integration, streaming |
|
||||
| `apps/server/src/providers/provider-factory.ts` | Model routing | `getProviderForModel()` pattern |
|
||||
| `apps/server/src/providers/types.ts` | Type definitions | `ProviderMessage`, `ExecuteOptions` |
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
# Files should exist and be readable
|
||||
cat apps/server/src/providers/base-provider.ts | head -50
|
||||
cat apps/server/src/providers/claude-provider.ts | head -100
|
||||
```
|
||||
|
||||
### Task 0.2: Read Service Integration
|
||||
|
||||
**Status:** `complete`
|
||||
|
||||
Understand how providers are consumed:
|
||||
|
||||
| File | Purpose | Key Patterns |
|
||||
| ----------------------------------------------- | --------------------- | ---------------------------------- |
|
||||
| `apps/server/src/services/agent-service.ts` | Chat sessions | Provider streaming, event emission |
|
||||
| `apps/server/src/services/auto-mode-service.ts` | Autonomous tasks | executeOptions, tool handling |
|
||||
| `apps/server/src/lib/sdk-options.ts` | Configuration factory | Tool presets, max turns |
|
||||
|
||||
### Task 0.3: Read UI Streaming/Logging
|
||||
|
||||
**Status:** `complete`
|
||||
|
||||
Understand log parsing and display:
|
||||
|
||||
| File | Purpose | Key Patterns |
|
||||
| ------------------------------------------ | ------------------ | ---------------------------- |
|
||||
| `apps/ui/src/lib/log-parser.ts` | Parse agent output | Entry types, tool categories |
|
||||
| `apps/ui/src/components/ui/log-viewer.tsx` | Display logs | Collapsible entries, search |
|
||||
|
||||
### Task 0.4: Read Setup Flow
|
||||
|
||||
**Status:** `complete`
|
||||
|
||||
Understand setup wizard patterns:
|
||||
|
||||
| File | Purpose | Key Patterns |
|
||||
| --------------------------------------------------- | ------------------ | ------------------------ |
|
||||
| `apps/server/src/routes/setup/index.ts` | Route registration | Handler patterns |
|
||||
| `apps/server/src/routes/setup/get-claude-status.ts` | CLI detection | Installation check logic |
|
||||
| `apps/ui/src/components/views/setup-view.tsx` | Wizard UI | Step components |
|
||||
|
||||
### Task 0.5: Read Types Package
|
||||
|
||||
**Status:** `complete`
|
||||
|
||||
Understand type definitions:
|
||||
|
||||
| File | Purpose | Key Patterns |
|
||||
| --------------------------------- | -------------- | ---------------------------- |
|
||||
| `libs/types/src/index.ts` | Re-exports | Export patterns |
|
||||
| `libs/types/src/settings.ts` | Settings types | `AIProfile`, `ModelProvider` |
|
||||
| `libs/types/src/model.ts` | Model aliases | `CLAUDE_MODEL_MAP` |
|
||||
| `libs/types/src/model-display.ts` | UI metadata | Display info pattern |
|
||||
|
||||
### Task 0.6: Document Cursor CLI Behavior
|
||||
|
||||
**Status:** `complete`
|
||||
|
||||
Test and document Cursor CLI behavior:
|
||||
|
||||
```bash
|
||||
# Check installation
|
||||
cursor-agent --version
|
||||
|
||||
# Check auth status (if available)
|
||||
cursor-agent status 2>&1 || echo "No status command"
|
||||
|
||||
# Test stream-json output (dry run)
|
||||
echo "Test prompt" | cursor-agent -p --output-format stream-json --model auto 2>&1 | head -20
|
||||
```
|
||||
|
||||
Document:
|
||||
|
||||
- [x] Exact event sequence for simple prompt
|
||||
- [x] Error message formats
|
||||
- [x] Exit codes for different failure modes
|
||||
- [x] How tool calls appear in stream
|
||||
|
||||
---
|
||||
|
||||
## Deliverable: Analysis Document
|
||||
|
||||
Create `docs/cursor-integration-analysis.md` with findings:
|
||||
|
||||
```markdown
|
||||
# Cursor CLI Integration Analysis
|
||||
|
||||
## Provider Pattern Summary
|
||||
|
||||
### BaseProvider Interface
|
||||
|
||||
- `executeQuery()` returns `AsyncGenerator<ProviderMessage>`
|
||||
- Messages must match format: { type, message?, result?, error? }
|
||||
- Session IDs propagated through all messages
|
||||
|
||||
### ClaudeProvider Patterns
|
||||
|
||||
- Uses Claude Agent SDK `query()` function
|
||||
- Streaming handled natively by SDK
|
||||
- Yields messages directly from SDK stream
|
||||
|
||||
### Key Interfaces
|
||||
|
||||
[Document: ProviderMessage, ExecuteOptions, InstallationStatus]
|
||||
|
||||
## Cursor CLI Behavior
|
||||
|
||||
### Stream Event Sequence
|
||||
|
||||
1. system/init - session start
|
||||
2. user - input prompt
|
||||
3. assistant - response text
|
||||
4. tool_call/started - tool invocation
|
||||
5. tool_call/completed - tool result
|
||||
6. result/success - final output
|
||||
|
||||
### Event Format Differences
|
||||
|
||||
[Document any transformations needed]
|
||||
|
||||
### Error Scenarios
|
||||
|
||||
- Not authenticated: [error message/code]
|
||||
- Rate limited: [error message/code]
|
||||
- Network error: [error message/code]
|
||||
|
||||
## Integration Points
|
||||
|
||||
### Files to Create
|
||||
|
||||
[List with descriptions]
|
||||
|
||||
### Files to Modify
|
||||
|
||||
[List with specific changes needed]
|
||||
|
||||
## Open Questions
|
||||
|
||||
[Any unresolved issues]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [x] All provider files read and understood
|
||||
- [x] Service integration patterns documented
|
||||
- [x] Log parser patterns understood
|
||||
- [x] Setup wizard flow mapped
|
||||
- [x] Types package structure documented
|
||||
- [x] Cursor CLI behavior tested (if installed)
|
||||
- [x] Analysis document created in `docs/`
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- This phase is **read-only** - no code changes
|
||||
- Document anything unclear for later clarification
|
||||
- Note any differences from the high-level plan provided
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Cursor CLI Output Format](https://cursor.com/docs/cli/reference/output-format)
|
||||
- [Cursor CLI Usage](https://cursor.com/docs/cli/using)
|
||||
- [Cursor CLI GitHub Actions](https://cursor.com/docs/cli/github-actions)
|
||||
@@ -1,443 +0,0 @@
|
||||
# Phase 1: Core Types & Configuration
|
||||
|
||||
**Status:** `completed`
|
||||
**Dependencies:** Phase 0 (Analysis)
|
||||
**Estimated Effort:** Small (type definitions only)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Define all Cursor-specific types and extend existing types to support the new provider.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 1.1: Create Cursor Model Definitions
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `libs/types/src/cursor-models.ts`
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Cursor CLI Model IDs
|
||||
* Reference: https://cursor.com/docs
|
||||
*/
|
||||
export type CursorModelId =
|
||||
| 'auto' // Auto-select best model
|
||||
| 'claude-sonnet-4' // Claude Sonnet 4
|
||||
| 'claude-sonnet-4-thinking' // Claude Sonnet 4 with extended thinking
|
||||
| 'gpt-4o' // GPT-4o
|
||||
| 'gpt-4o-mini' // GPT-4o Mini
|
||||
| 'gemini-2.5-pro' // Gemini 2.5 Pro
|
||||
| 'o3-mini'; // O3 Mini
|
||||
|
||||
/**
|
||||
* Cursor model metadata
|
||||
*/
|
||||
export interface CursorModelConfig {
|
||||
id: CursorModelId;
|
||||
label: string;
|
||||
description: string;
|
||||
hasThinking: boolean;
|
||||
tier: 'free' | 'pro';
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete model map for Cursor CLI
|
||||
*/
|
||||
export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
|
||||
auto: {
|
||||
id: 'auto',
|
||||
label: 'Auto (Recommended)',
|
||||
description: 'Automatically selects the best model for each task',
|
||||
hasThinking: false,
|
||||
tier: 'free',
|
||||
},
|
||||
'claude-sonnet-4': {
|
||||
id: 'claude-sonnet-4',
|
||||
label: 'Claude Sonnet 4',
|
||||
description: 'Anthropic Claude Sonnet 4 via Cursor',
|
||||
hasThinking: false,
|
||||
tier: 'pro',
|
||||
},
|
||||
'claude-sonnet-4-thinking': {
|
||||
id: 'claude-sonnet-4-thinking',
|
||||
label: 'Claude Sonnet 4 (Thinking)',
|
||||
description: 'Claude Sonnet 4 with extended thinking enabled',
|
||||
hasThinking: true,
|
||||
tier: 'pro',
|
||||
},
|
||||
'gpt-4o': {
|
||||
id: 'gpt-4o',
|
||||
label: 'GPT-4o',
|
||||
description: 'OpenAI GPT-4o via Cursor',
|
||||
hasThinking: false,
|
||||
tier: 'pro',
|
||||
},
|
||||
'gpt-4o-mini': {
|
||||
id: 'gpt-4o-mini',
|
||||
label: 'GPT-4o Mini',
|
||||
description: 'OpenAI GPT-4o Mini (faster, cheaper)',
|
||||
hasThinking: false,
|
||||
tier: 'free',
|
||||
},
|
||||
'gemini-2.5-pro': {
|
||||
id: 'gemini-2.5-pro',
|
||||
label: 'Gemini 2.5 Pro',
|
||||
description: 'Google Gemini 2.5 Pro via Cursor',
|
||||
hasThinking: false,
|
||||
tier: 'pro',
|
||||
},
|
||||
'o3-mini': {
|
||||
id: 'o3-mini',
|
||||
label: 'O3 Mini',
|
||||
description: 'OpenAI O3 Mini reasoning model',
|
||||
hasThinking: true,
|
||||
tier: 'pro',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper: Check if model has thinking capability
|
||||
*/
|
||||
export function cursorModelHasThinking(modelId: CursorModelId): boolean {
|
||||
return CURSOR_MODEL_MAP[modelId]?.hasThinking ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Get display name for model
|
||||
*/
|
||||
export function getCursorModelLabel(modelId: CursorModelId): string {
|
||||
return CURSOR_MODEL_MAP[modelId]?.label ?? modelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Get all cursor model IDs
|
||||
*/
|
||||
export function getAllCursorModelIds(): CursorModelId[] {
|
||||
return Object.keys(CURSOR_MODEL_MAP) as CursorModelId[];
|
||||
}
|
||||
```
|
||||
|
||||
### Task 1.2: Create Cursor CLI Types
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `libs/types/src/cursor-cli.ts`
|
||||
|
||||
```typescript
|
||||
import { CursorModelId } from './cursor-models';
|
||||
|
||||
/**
|
||||
* Cursor CLI configuration file schema
|
||||
* Stored in: .automaker/cursor-config.json
|
||||
*/
|
||||
export interface CursorCliConfig {
|
||||
defaultModel?: CursorModelId;
|
||||
models?: CursorModelId[]; // Enabled models
|
||||
mcpServers?: string[]; // MCP server configs to load
|
||||
rules?: string[]; // .cursor/rules paths
|
||||
}
|
||||
|
||||
/**
|
||||
* Cursor authentication status
|
||||
*/
|
||||
export interface CursorAuthStatus {
|
||||
authenticated: boolean;
|
||||
method: 'login' | 'api_key' | 'none';
|
||||
hasCredentialsFile?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Reuse existing InstallationStatus from provider.ts
|
||||
* The existing type already has: installed, path, version, method, hasApiKey, authenticated
|
||||
*
|
||||
* Add 'login' to the method union if needed:
|
||||
* method?: 'cli' | 'npm' | 'brew' | 'sdk' | 'login';
|
||||
*/
|
||||
|
||||
/**
|
||||
* Cursor stream-json event types (from CLI output)
|
||||
*/
|
||||
export interface CursorSystemEvent {
|
||||
type: 'system';
|
||||
subtype: 'init';
|
||||
apiKeySource: 'env' | 'flag' | 'login';
|
||||
cwd: string;
|
||||
session_id: string;
|
||||
model: string;
|
||||
permissionMode: string;
|
||||
}
|
||||
|
||||
export interface CursorUserEvent {
|
||||
type: 'user';
|
||||
message: {
|
||||
role: 'user';
|
||||
content: Array<{ type: 'text'; text: string }>;
|
||||
};
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
export interface CursorAssistantEvent {
|
||||
type: 'assistant';
|
||||
message: {
|
||||
role: 'assistant';
|
||||
content: Array<{ type: 'text'; text: string }>;
|
||||
};
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
export interface CursorToolCallEvent {
|
||||
type: 'tool_call';
|
||||
subtype: 'started' | 'completed';
|
||||
call_id: string;
|
||||
tool_call: {
|
||||
readToolCall?: {
|
||||
args: { path: string };
|
||||
result?: {
|
||||
success?: {
|
||||
content: string;
|
||||
isEmpty: boolean;
|
||||
exceededLimit: boolean;
|
||||
totalLines: number;
|
||||
totalChars: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
writeToolCall?: {
|
||||
args: { path: string; fileText: string; toolCallId?: string };
|
||||
result?: {
|
||||
success?: {
|
||||
path: string;
|
||||
linesCreated: number;
|
||||
fileSize: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
function?: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
};
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
export interface CursorResultEvent {
|
||||
type: 'result';
|
||||
subtype: 'success' | 'error';
|
||||
duration_ms: number;
|
||||
duration_api_ms: number;
|
||||
is_error: boolean;
|
||||
result: string;
|
||||
session_id: string;
|
||||
request_id?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type CursorStreamEvent =
|
||||
| CursorSystemEvent
|
||||
| CursorUserEvent
|
||||
| CursorAssistantEvent
|
||||
| CursorToolCallEvent
|
||||
| CursorResultEvent;
|
||||
```
|
||||
|
||||
### Task 1.3: Extend ModelProvider Type
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `libs/types/src/settings.ts`
|
||||
|
||||
Find and update:
|
||||
|
||||
```typescript
|
||||
// BEFORE:
|
||||
export type ModelProvider = 'claude';
|
||||
|
||||
// AFTER:
|
||||
export type ModelProvider = 'claude' | 'cursor';
|
||||
```
|
||||
|
||||
### Task 1.4: Add Cursor Profile Config Type
|
||||
|
||||
**Status:** `skipped` (not needed - thinking is embedded in model ID)
|
||||
|
||||
**File:** `libs/types/src/settings.ts`
|
||||
|
||||
Add after existing AIProfile interface:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Cursor-specific profile configuration
|
||||
* Note: For Cursor, thinking is embedded in model ID (e.g., 'claude-sonnet-4-thinking')
|
||||
*/
|
||||
export interface CursorProfileConfig {
|
||||
model: CursorModelId;
|
||||
// No separate thinkingLevel needed - embedded in model ID
|
||||
}
|
||||
```
|
||||
|
||||
### Task 1.5: Update ModelOption Interface
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `libs/types/src/model-display.ts`
|
||||
|
||||
Update the hardcoded provider type to use ModelProvider:
|
||||
|
||||
```typescript
|
||||
// BEFORE (line 24):
|
||||
export interface ModelOption {
|
||||
id: AgentModel;
|
||||
label: string;
|
||||
description: string;
|
||||
badge?: string;
|
||||
provider: 'claude'; // ❌ Hardcoded
|
||||
}
|
||||
|
||||
// AFTER:
|
||||
import { ModelProvider } from './settings.js';
|
||||
|
||||
export interface ModelOption {
|
||||
id: AgentModel | CursorModelId; // Union for both providers
|
||||
label: string;
|
||||
description: string;
|
||||
badge?: string;
|
||||
provider: ModelProvider; // ✅ Supports both 'claude' and 'cursor'
|
||||
}
|
||||
```
|
||||
|
||||
### Task 1.6: Extend DEFAULT_MODELS
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `libs/types/src/model.ts`
|
||||
|
||||
Add cursor default model:
|
||||
|
||||
```typescript
|
||||
// BEFORE:
|
||||
export const DEFAULT_MODELS = {
|
||||
claude: 'claude-opus-4-5-20251101',
|
||||
} as const;
|
||||
|
||||
// AFTER:
|
||||
export const DEFAULT_MODELS = {
|
||||
claude: 'claude-opus-4-5-20251101',
|
||||
cursor: 'auto', // Cursor's recommended default
|
||||
} as const;
|
||||
```
|
||||
|
||||
### Task 1.7: Update Type Exports
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `libs/types/src/index.ts`
|
||||
|
||||
Add exports:
|
||||
|
||||
```typescript
|
||||
// Cursor types
|
||||
export * from './cursor-models.js';
|
||||
export * from './cursor-cli.js';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Type Compilation
|
||||
|
||||
```bash
|
||||
cd libs/types
|
||||
pnpm build
|
||||
```
|
||||
|
||||
**Expected:** No compilation errors
|
||||
|
||||
### Test 2: Import Check
|
||||
|
||||
Create a temporary test file:
|
||||
|
||||
```typescript
|
||||
// test-cursor-types.ts
|
||||
import {
|
||||
CursorModelId,
|
||||
CursorModelConfig,
|
||||
CURSOR_MODEL_MAP,
|
||||
cursorModelHasThinking,
|
||||
CursorStreamEvent,
|
||||
CursorCliConfig,
|
||||
ModelProvider,
|
||||
} from '@automaker/types';
|
||||
|
||||
// Should compile without errors
|
||||
const model: CursorModelId = 'claude-sonnet-4';
|
||||
const provider: ModelProvider = 'cursor';
|
||||
const hasThinking = cursorModelHasThinking('claude-sonnet-4-thinking');
|
||||
console.log(model, provider, hasThinking);
|
||||
```
|
||||
|
||||
```bash
|
||||
npx tsc test-cursor-types.ts --noEmit
|
||||
rm test-cursor-types.ts
|
||||
```
|
||||
|
||||
**Expected:** No errors
|
||||
|
||||
### Test 3: Model Map Validity
|
||||
|
||||
```typescript
|
||||
// In Node REPL or test file
|
||||
import { CURSOR_MODEL_MAP, CursorModelId } from '@automaker/types';
|
||||
|
||||
const modelIds = Object.keys(CURSOR_MODEL_MAP) as CursorModelId[];
|
||||
console.log('Models:', modelIds.length);
|
||||
|
||||
// All models should have required fields
|
||||
for (const [id, config] of Object.entries(CURSOR_MODEL_MAP)) {
|
||||
console.assert(config.id === id, `ID mismatch: ${id}`);
|
||||
console.assert(typeof config.label === 'string', `Missing label: ${id}`);
|
||||
console.assert(typeof config.hasThinking === 'boolean', `Missing hasThinking: ${id}`);
|
||||
console.assert(['free', 'pro'].includes(config.tier), `Invalid tier: ${id}`);
|
||||
}
|
||||
console.log('All models valid');
|
||||
```
|
||||
|
||||
**Expected:** All assertions pass
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [ ] `libs/types/src/cursor-models.ts` created with all model definitions
|
||||
- [ ] `libs/types/src/cursor-cli.ts` created with CLI types
|
||||
- [ ] `libs/types/src/settings.ts` extended with `cursor` provider
|
||||
- [ ] `libs/types/src/index.ts` exports new types
|
||||
- [ ] `pnpm build` succeeds in libs/types
|
||||
- [ ] No TypeScript errors in dependent packages
|
||||
- [ ] Model map contains all expected models
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| --------------------------------- | ------ | ----------------------------- |
|
||||
| `libs/types/src/cursor-models.ts` | Create | Model definitions and helpers |
|
||||
| `libs/types/src/cursor-cli.ts` | Create | CLI and stream event types |
|
||||
| `libs/types/src/settings.ts` | Modify | Add `cursor` to ModelProvider |
|
||||
| `libs/types/src/index.ts` | Modify | Export new types |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Model IDs may need updating as Cursor adds/removes models
|
||||
- The `hasThinking` property is critical for UI display
|
||||
- Stream event types must match actual CLI output exactly
|
||||
@@ -1,649 +0,0 @@
|
||||
# Phase 10: Testing & Validation
|
||||
|
||||
**Status:** `pending`
|
||||
**Dependencies:** All previous phases
|
||||
**Estimated Effort:** Medium (comprehensive testing)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Create comprehensive tests and perform validation to ensure the Cursor CLI integration works correctly across all scenarios.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 10.1: Unit Tests - Cursor Provider
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
**File:** `apps/server/tests/unit/providers/cursor-provider.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { CursorProvider, CursorErrorCode } from '../../../src/providers/cursor-provider';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// Mock child_process
|
||||
vi.mock('child_process', () => ({
|
||||
execSync: vi.fn(),
|
||||
spawn: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock fs
|
||||
vi.mock('fs', () => ({
|
||||
existsSync: vi.fn(),
|
||||
readFileSync: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('CursorProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getName', () => {
|
||||
it('should return "cursor"', () => {
|
||||
const provider = new CursorProvider();
|
||||
expect(provider.getName()).toBe('cursor');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInstalled', () => {
|
||||
it('should return true when CLI is found in PATH', async () => {
|
||||
vi.mocked(execSync).mockReturnValue('/usr/local/bin/cursor-agent\n');
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
|
||||
const provider = new CursorProvider();
|
||||
const result = await provider.isInstalled();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when CLI is not found', async () => {
|
||||
vi.mocked(execSync).mockImplementation(() => {
|
||||
throw new Error('not found');
|
||||
});
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
const provider = new CursorProvider();
|
||||
const result = await provider.isInstalled();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkAuth', () => {
|
||||
it('should detect API key authentication', async () => {
|
||||
process.env.CURSOR_API_KEY = 'test-key';
|
||||
|
||||
const provider = new CursorProvider();
|
||||
const result = await provider.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(true);
|
||||
expect(result.method).toBe('api_key');
|
||||
|
||||
delete process.env.CURSOR_API_KEY;
|
||||
});
|
||||
|
||||
it('should detect login authentication from credentials file', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ accessToken: 'token' }));
|
||||
|
||||
const provider = new CursorProvider();
|
||||
const result = await provider.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(true);
|
||||
expect(result.method).toBe('login');
|
||||
});
|
||||
|
||||
it('should return not authenticated when no credentials', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
const provider = new CursorProvider();
|
||||
const result = await provider.checkAuth();
|
||||
|
||||
expect(result.authenticated).toBe(false);
|
||||
expect(result.method).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseStreamLine', () => {
|
||||
it('should parse valid JSON event', () => {
|
||||
const provider = new CursorProvider();
|
||||
const line = '{"type":"system","subtype":"init","session_id":"abc"}';
|
||||
|
||||
const result = (provider as any).parseStreamLine(line);
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'system',
|
||||
subtype: 'init',
|
||||
session_id: 'abc',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for invalid JSON', () => {
|
||||
const provider = new CursorProvider();
|
||||
const result = (provider as any).parseStreamLine('not json');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for empty lines', () => {
|
||||
const provider = new CursorProvider();
|
||||
expect((provider as any).parseStreamLine('')).toBeNull();
|
||||
expect((provider as any).parseStreamLine(' ')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapError', () => {
|
||||
it('should map authentication errors', () => {
|
||||
const provider = new CursorProvider();
|
||||
|
||||
const error = (provider as any).mapError('Error: not authenticated', 1);
|
||||
|
||||
expect(error.code).toBe(CursorErrorCode.NOT_AUTHENTICATED);
|
||||
expect(error.recoverable).toBe(true);
|
||||
expect(error.suggestion).toBeDefined();
|
||||
});
|
||||
|
||||
it('should map rate limit errors', () => {
|
||||
const provider = new CursorProvider();
|
||||
|
||||
const error = (provider as any).mapError('Rate limit exceeded', 1);
|
||||
|
||||
expect(error.code).toBe(CursorErrorCode.RATE_LIMITED);
|
||||
expect(error.recoverable).toBe(true);
|
||||
});
|
||||
|
||||
it('should map network errors', () => {
|
||||
const provider = new CursorProvider();
|
||||
|
||||
const error = (provider as any).mapError('ECONNREFUSED', 1);
|
||||
|
||||
expect(error.code).toBe(CursorErrorCode.NETWORK_ERROR);
|
||||
expect(error.recoverable).toBe(true);
|
||||
});
|
||||
|
||||
it('should return unknown error for unrecognized messages', () => {
|
||||
const provider = new CursorProvider();
|
||||
|
||||
const error = (provider as any).mapError('Something weird happened', 1);
|
||||
|
||||
expect(error.code).toBe(CursorErrorCode.UNKNOWN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvailableModels', () => {
|
||||
it('should return all Cursor models', () => {
|
||||
const provider = new CursorProvider();
|
||||
const models = provider.getAvailableModels();
|
||||
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
expect(models.every((m) => m.provider === 'cursor')).toBe(true);
|
||||
expect(models.some((m) => m.id.includes('auto'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Task 10.2: Unit Tests - Provider Factory
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
**File:** `apps/server/tests/unit/providers/provider-factory.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ProviderFactory } from '../../../src/providers/provider-factory';
|
||||
import { ClaudeProvider } from '../../../src/providers/claude-provider';
|
||||
import { CursorProvider } from '../../../src/providers/cursor-provider';
|
||||
|
||||
describe('ProviderFactory', () => {
|
||||
describe('getProviderNameForModel', () => {
|
||||
it('should route cursor-prefixed models to cursor', () => {
|
||||
expect(ProviderFactory.getProviderNameForModel('cursor-auto')).toBe('cursor');
|
||||
expect(ProviderFactory.getProviderNameForModel('cursor-gpt-4o')).toBe('cursor');
|
||||
expect(ProviderFactory.getProviderNameForModel('cursor-claude-sonnet-4')).toBe('cursor');
|
||||
});
|
||||
|
||||
it('should route claude models to claude', () => {
|
||||
expect(ProviderFactory.getProviderNameForModel('claude-sonnet-4')).toBe('claude');
|
||||
expect(ProviderFactory.getProviderNameForModel('opus')).toBe('claude');
|
||||
expect(ProviderFactory.getProviderNameForModel('sonnet')).toBe('claude');
|
||||
expect(ProviderFactory.getProviderNameForModel('haiku')).toBe('claude');
|
||||
});
|
||||
|
||||
it('should default unknown models to claude', () => {
|
||||
expect(ProviderFactory.getProviderNameForModel('unknown-model')).toBe('claude');
|
||||
expect(ProviderFactory.getProviderNameForModel('random')).toBe('claude');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviderForModel', () => {
|
||||
it('should return CursorProvider for cursor models', () => {
|
||||
const provider = ProviderFactory.getProviderForModel('cursor-auto');
|
||||
expect(provider).toBeInstanceOf(CursorProvider);
|
||||
expect(provider.getName()).toBe('cursor');
|
||||
});
|
||||
|
||||
it('should return ClaudeProvider for claude models', () => {
|
||||
const provider = ProviderFactory.getProviderForModel('sonnet');
|
||||
expect(provider).toBeInstanceOf(ClaudeProvider);
|
||||
expect(provider.getName()).toBe('claude');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllProviders', () => {
|
||||
it('should return both providers', () => {
|
||||
const providers = ProviderFactory.getAllProviders();
|
||||
const names = providers.map((p) => p.getName());
|
||||
|
||||
expect(names).toContain('claude');
|
||||
expect(names).toContain('cursor');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProviderByName', () => {
|
||||
it('should return correct provider by name', () => {
|
||||
expect(ProviderFactory.getProviderByName('cursor')?.getName()).toBe('cursor');
|
||||
expect(ProviderFactory.getProviderByName('claude')?.getName()).toBe('claude');
|
||||
expect(ProviderFactory.getProviderByName('unknown')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllAvailableModels', () => {
|
||||
it('should include models from all providers', () => {
|
||||
const models = ProviderFactory.getAllAvailableModels();
|
||||
|
||||
const cursorModels = models.filter((m) => m.provider === 'cursor');
|
||||
const claudeModels = models.filter((m) => m.provider === 'claude');
|
||||
|
||||
expect(cursorModels.length).toBeGreaterThan(0);
|
||||
expect(claudeModels.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Task 10.3: Unit Tests - Types
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
**File:** `libs/types/tests/cursor-types.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
CURSOR_MODEL_MAP,
|
||||
cursorModelHasThinking,
|
||||
getCursorModelLabel,
|
||||
getAllCursorModelIds,
|
||||
CursorModelId,
|
||||
} from '../src/cursor-models';
|
||||
import { profileHasThinking, getProfileModelString, AIProfile } from '../src/settings';
|
||||
|
||||
describe('Cursor Model Types', () => {
|
||||
describe('CURSOR_MODEL_MAP', () => {
|
||||
it('should have all required models', () => {
|
||||
const requiredModels: CursorModelId[] = [
|
||||
'auto',
|
||||
'claude-sonnet-4',
|
||||
'claude-sonnet-4-thinking',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
];
|
||||
|
||||
for (const model of requiredModels) {
|
||||
expect(CURSOR_MODEL_MAP[model]).toBeDefined();
|
||||
expect(CURSOR_MODEL_MAP[model].id).toBe(model);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have valid tier values', () => {
|
||||
for (const config of Object.values(CURSOR_MODEL_MAP)) {
|
||||
expect(['free', 'pro']).toContain(config.tier);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('cursorModelHasThinking', () => {
|
||||
it('should return true for thinking models', () => {
|
||||
expect(cursorModelHasThinking('claude-sonnet-4-thinking')).toBe(true);
|
||||
expect(cursorModelHasThinking('o3-mini')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-thinking models', () => {
|
||||
expect(cursorModelHasThinking('auto')).toBe(false);
|
||||
expect(cursorModelHasThinking('gpt-4o')).toBe(false);
|
||||
expect(cursorModelHasThinking('claude-sonnet-4')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCursorModelLabel', () => {
|
||||
it('should return correct labels', () => {
|
||||
expect(getCursorModelLabel('auto')).toBe('Auto (Recommended)');
|
||||
expect(getCursorModelLabel('gpt-4o')).toBe('GPT-4o');
|
||||
});
|
||||
|
||||
it('should return model ID for unknown models', () => {
|
||||
expect(getCursorModelLabel('unknown' as CursorModelId)).toBe('unknown');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Profile Helpers', () => {
|
||||
describe('profileHasThinking', () => {
|
||||
it('should detect Claude thinking levels', () => {
|
||||
const profile: AIProfile = {
|
||||
id: '1',
|
||||
name: 'Test',
|
||||
description: '',
|
||||
provider: 'claude',
|
||||
model: 'sonnet',
|
||||
thinkingLevel: 'high',
|
||||
isBuiltIn: false,
|
||||
};
|
||||
expect(profileHasThinking(profile)).toBe(true);
|
||||
|
||||
profile.thinkingLevel = 'none';
|
||||
expect(profileHasThinking(profile)).toBe(false);
|
||||
});
|
||||
|
||||
it('should detect Cursor thinking models', () => {
|
||||
const profile: AIProfile = {
|
||||
id: '1',
|
||||
name: 'Test',
|
||||
description: '',
|
||||
provider: 'cursor',
|
||||
cursorModel: 'claude-sonnet-4-thinking',
|
||||
isBuiltIn: false,
|
||||
};
|
||||
expect(profileHasThinking(profile)).toBe(true);
|
||||
|
||||
profile.cursorModel = 'gpt-4o';
|
||||
expect(profileHasThinking(profile)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProfileModelString', () => {
|
||||
it('should format Cursor models correctly', () => {
|
||||
const profile: AIProfile = {
|
||||
id: '1',
|
||||
name: 'Test',
|
||||
description: '',
|
||||
provider: 'cursor',
|
||||
cursorModel: 'gpt-4o',
|
||||
isBuiltIn: false,
|
||||
};
|
||||
expect(getProfileModelString(profile)).toBe('cursor-gpt-4o');
|
||||
});
|
||||
|
||||
it('should format Claude models correctly', () => {
|
||||
const profile: AIProfile = {
|
||||
id: '1',
|
||||
name: 'Test',
|
||||
description: '',
|
||||
provider: 'claude',
|
||||
model: 'sonnet',
|
||||
isBuiltIn: false,
|
||||
};
|
||||
expect(getProfileModelString(profile)).toBe('sonnet');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Task 10.4: Integration Tests
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
**File:** `apps/server/tests/integration/cursor-integration.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { CursorProvider } from '../../src/providers/cursor-provider';
|
||||
import { ProviderFactory } from '../../src/providers/provider-factory';
|
||||
|
||||
describe('Cursor Integration (requires cursor-agent)', () => {
|
||||
let provider: CursorProvider;
|
||||
let isInstalled: boolean;
|
||||
|
||||
beforeAll(async () => {
|
||||
provider = new CursorProvider();
|
||||
isInstalled = await provider.isInstalled();
|
||||
});
|
||||
|
||||
describe('when cursor-agent is installed', () => {
|
||||
it.skipIf(!isInstalled)('should get version', async () => {
|
||||
const version = await provider.getVersion();
|
||||
expect(version).toBeTruthy();
|
||||
expect(typeof version).toBe('string');
|
||||
});
|
||||
|
||||
it.skipIf(!isInstalled)('should check auth status', async () => {
|
||||
const auth = await provider.checkAuth();
|
||||
expect(auth).toHaveProperty('authenticated');
|
||||
expect(auth).toHaveProperty('method');
|
||||
});
|
||||
|
||||
it.skipIf(!isInstalled)('should detect installation', async () => {
|
||||
const status = await provider.detectInstallation();
|
||||
expect(status.installed).toBe(true);
|
||||
expect(status.path).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when cursor-agent is not installed', () => {
|
||||
it.skipIf(isInstalled)('should report not installed', async () => {
|
||||
const status = await provider.detectInstallation();
|
||||
expect(status.installed).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Task 10.5: E2E Tests
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
**File:** `apps/ui/tests/e2e/cursor-setup.spec.ts`
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Cursor Setup Wizard', () => {
|
||||
test('should show Cursor setup step', async ({ page }) => {
|
||||
// Navigate to setup (fresh install)
|
||||
await page.goto('/setup');
|
||||
|
||||
// Wait for Cursor step to appear
|
||||
await expect(page.getByText('Cursor CLI Setup')).toBeVisible();
|
||||
await expect(page.getByText('Optional')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should allow skipping Cursor setup', async ({ page }) => {
|
||||
await page.goto('/setup');
|
||||
|
||||
// Find and click skip button
|
||||
await page.getByRole('button', { name: 'Skip for now' }).click();
|
||||
|
||||
// Should proceed to next step
|
||||
await expect(page.getByText('Cursor CLI Setup')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should show installation instructions when not installed', async ({ page }) => {
|
||||
await page.goto('/setup');
|
||||
|
||||
// Check for install command
|
||||
await expect(page.getByText('curl https://cursor.com/install')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Cursor Settings', () => {
|
||||
test('should show Cursor tab in settings', async ({ page }) => {
|
||||
await page.goto('/settings/providers');
|
||||
|
||||
// Should have tabs for both providers
|
||||
await expect(page.getByRole('tab', { name: 'Claude' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'Cursor' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should switch between provider tabs', async ({ page }) => {
|
||||
await page.goto('/settings/providers');
|
||||
|
||||
// Click Cursor tab
|
||||
await page.getByRole('tab', { name: 'Cursor' }).click();
|
||||
|
||||
// Should show Cursor settings
|
||||
await expect(page.getByText('Cursor CLI Status')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Task 10.6: Manual Testing Checklist
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
Create a manual testing checklist:
|
||||
|
||||
```markdown
|
||||
## Manual Testing Checklist
|
||||
|
||||
### Setup Flow
|
||||
|
||||
- [ ] Fresh install shows Cursor step
|
||||
- [ ] Can skip Cursor setup
|
||||
- [ ] Installation status is accurate
|
||||
- [ ] Login flow works (copy command, poll for auth)
|
||||
- [ ] Refresh button updates status
|
||||
|
||||
### Settings
|
||||
|
||||
- [ ] Provider tabs work
|
||||
- [ ] Cursor status shows correctly
|
||||
- [ ] Model selection works
|
||||
- [ ] Default model saves
|
||||
- [ ] Enabled models save
|
||||
|
||||
### Profiles
|
||||
|
||||
- [ ] Can create Cursor profile
|
||||
- [ ] Provider switch resets options
|
||||
- [ ] Cursor models show thinking badge
|
||||
- [ ] Built-in Cursor profiles appear
|
||||
- [ ] Profile cards show provider info
|
||||
|
||||
### Execution
|
||||
|
||||
- [ ] Tasks with Cursor models execute
|
||||
- [ ] Streaming works correctly
|
||||
- [ ] Tool calls are displayed
|
||||
- [ ] Errors show suggestions
|
||||
- [ ] Can abort Cursor tasks
|
||||
|
||||
### Log Viewer
|
||||
|
||||
- [ ] Cursor events parsed correctly
|
||||
- [ ] Tool calls categorized
|
||||
- [ ] File paths highlighted
|
||||
- [ ] Provider badge shown
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- [ ] Switch provider mid-session
|
||||
- [ ] Cursor not installed handling
|
||||
- [ ] Network errors handled
|
||||
- [ ] Rate limiting handled
|
||||
- [ ] Auth expired handling
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Run All Unit Tests
|
||||
|
||||
```bash
|
||||
pnpm test:unit
|
||||
```
|
||||
|
||||
All tests should pass.
|
||||
|
||||
### Test 2: Run Integration Tests
|
||||
|
||||
```bash
|
||||
pnpm test:integration
|
||||
```
|
||||
|
||||
Tests requiring cursor-agent will be skipped if not installed.
|
||||
|
||||
### Test 3: Run E2E Tests
|
||||
|
||||
```bash
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
Browser tests should pass.
|
||||
|
||||
### Test 4: Type Check
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
No TypeScript errors.
|
||||
|
||||
### Test 5: Lint Check
|
||||
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
No linting errors.
|
||||
|
||||
### Test 6: Build
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Build should succeed without errors.
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [ ] Unit tests pass (cursor-provider)
|
||||
- [ ] Unit tests pass (provider-factory)
|
||||
- [ ] Unit tests pass (types)
|
||||
- [ ] Integration tests pass (or skip if not installed)
|
||||
- [ ] E2E tests pass
|
||||
- [ ] Manual testing checklist completed
|
||||
- [ ] No TypeScript errors
|
||||
- [ ] No linting errors
|
||||
- [ ] Build succeeds
|
||||
- [ ] Documentation updated
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| ----------------------------------------------------------- | ------ | ----------------- |
|
||||
| `apps/server/tests/unit/providers/cursor-provider.test.ts` | Create | Provider tests |
|
||||
| `apps/server/tests/unit/providers/provider-factory.test.ts` | Create | Factory tests |
|
||||
| `libs/types/tests/cursor-types.test.ts` | Create | Type tests |
|
||||
| `apps/server/tests/integration/cursor-integration.test.ts` | Create | Integration tests |
|
||||
| `apps/ui/tests/e2e/cursor-setup.spec.ts` | Create | E2E tests |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Integration tests may be skipped if cursor-agent is not installed
|
||||
- E2E tests should work regardless of cursor-agent installation
|
||||
- Manual testing should cover both installed and not-installed scenarios
|
||||
@@ -1,850 +0,0 @@
|
||||
# Phase 2: Cursor Provider Implementation
|
||||
|
||||
**Status:** `completed`
|
||||
**Dependencies:** Phase 1 (Types)
|
||||
**Estimated Effort:** Medium-Large (core implementation)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the main `CursorProvider` class that spawns the cursor-agent CLI and streams responses in the AutoMaker provider format.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 2.1: Create Cursor Provider
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/server/src/providers/cursor-provider.ts`
|
||||
|
||||
```typescript
|
||||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { BaseProvider } from './base-provider';
|
||||
import {
|
||||
ProviderConfig,
|
||||
ExecuteOptions,
|
||||
ProviderMessage,
|
||||
InstallationStatus,
|
||||
ModelDefinition,
|
||||
} from './types';
|
||||
import {
|
||||
CursorModelId,
|
||||
CursorStreamEvent,
|
||||
CursorSystemEvent,
|
||||
CursorAssistantEvent,
|
||||
CursorToolCallEvent,
|
||||
CursorResultEvent,
|
||||
CURSOR_MODEL_MAP,
|
||||
CursorAuthStatus,
|
||||
} from '@automaker/types';
|
||||
import { createLogger, isAbortError } from '@automaker/utils';
|
||||
import { spawnJSONLProcess, type SubprocessOptions } from '@automaker/platform';
|
||||
|
||||
// Create logger for this module
|
||||
const logger = createLogger('CursorProvider');
|
||||
|
||||
/**
|
||||
* Cursor-specific error codes for detailed error handling
|
||||
*/
|
||||
export enum CursorErrorCode {
|
||||
NOT_INSTALLED = 'CURSOR_NOT_INSTALLED',
|
||||
NOT_AUTHENTICATED = 'CURSOR_NOT_AUTHENTICATED',
|
||||
RATE_LIMITED = 'CURSOR_RATE_LIMITED',
|
||||
MODEL_UNAVAILABLE = 'CURSOR_MODEL_UNAVAILABLE',
|
||||
NETWORK_ERROR = 'CURSOR_NETWORK_ERROR',
|
||||
PROCESS_CRASHED = 'CURSOR_PROCESS_CRASHED',
|
||||
TIMEOUT = 'CURSOR_TIMEOUT',
|
||||
UNKNOWN = 'CURSOR_UNKNOWN_ERROR',
|
||||
}
|
||||
|
||||
export interface CursorError extends Error {
|
||||
code: CursorErrorCode;
|
||||
recoverable: boolean;
|
||||
suggestion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CursorProvider - Integrates cursor-agent CLI as an AI provider
|
||||
*
|
||||
* Uses the cursor-agent CLI with --output-format stream-json for streaming responses.
|
||||
* Normalizes Cursor events to the AutoMaker ProviderMessage format.
|
||||
*/
|
||||
export class CursorProvider extends BaseProvider {
|
||||
private static CLI_NAME = 'cursor-agent';
|
||||
|
||||
/**
|
||||
* Installation paths based on official cursor-agent install script:
|
||||
*
|
||||
* Linux/macOS:
|
||||
* - Binary: ~/.local/share/cursor-agent/versions/<version>/cursor-agent
|
||||
* - Symlink: ~/.local/bin/cursor-agent -> versions/<version>/cursor-agent
|
||||
*
|
||||
* The install script creates versioned folders like:
|
||||
* ~/.local/share/cursor-agent/versions/2025.12.17-996666f/cursor-agent
|
||||
* And symlinks to ~/.local/bin/cursor-agent
|
||||
*/
|
||||
private static COMMON_PATHS: Record<string, string[]> = {
|
||||
linux: [
|
||||
path.join(os.homedir(), '.local/bin/cursor-agent'), // Primary symlink location
|
||||
'/usr/local/bin/cursor-agent',
|
||||
],
|
||||
darwin: [
|
||||
path.join(os.homedir(), '.local/bin/cursor-agent'), // Primary symlink location
|
||||
'/usr/local/bin/cursor-agent',
|
||||
],
|
||||
win32: [
|
||||
path.join(os.homedir(), 'AppData/Local/Programs/cursor-agent/cursor-agent.exe'),
|
||||
path.join(os.homedir(), '.local/bin/cursor-agent.exe'),
|
||||
'C:\\Program Files\\cursor-agent\\cursor-agent.exe',
|
||||
],
|
||||
};
|
||||
|
||||
// Version data directory where cursor-agent stores versions
|
||||
private static VERSIONS_DIR = path.join(os.homedir(), '.local/share/cursor-agent/versions');
|
||||
|
||||
private cliPath: string | null = null;
|
||||
private currentProcess: ChildProcess | null = null;
|
||||
|
||||
constructor(config: ProviderConfig = {}) {
|
||||
super(config);
|
||||
this.cliPath = config.cliPath || this.findCliPath();
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'cursor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Find cursor-agent CLI in PATH or common installation locations
|
||||
*/
|
||||
private findCliPath(): string | null {
|
||||
// Try 'which' / 'where' first
|
||||
try {
|
||||
const cmd = process.platform === 'win32' ? 'where cursor-agent' : 'which cursor-agent';
|
||||
const result = execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim().split('\n')[0];
|
||||
if (result && fs.existsSync(result)) {
|
||||
return result;
|
||||
}
|
||||
} catch {
|
||||
// Not in PATH
|
||||
}
|
||||
|
||||
// Check common installation paths for current platform
|
||||
const platform = process.platform as 'linux' | 'darwin' | 'win32';
|
||||
const platformPaths = CursorProvider.COMMON_PATHS[platform] || [];
|
||||
|
||||
for (const p of platformPaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check versions directory for any installed version
|
||||
if (fs.existsSync(CursorProvider.VERSIONS_DIR)) {
|
||||
try {
|
||||
const versions = fs
|
||||
.readdirSync(CursorProvider.VERSIONS_DIR)
|
||||
.filter((v) => !v.startsWith('.'))
|
||||
.sort()
|
||||
.reverse(); // Most recent first
|
||||
|
||||
for (const version of versions) {
|
||||
const binaryName = platform === 'win32' ? 'cursor-agent.exe' : 'cursor-agent';
|
||||
const versionPath = path.join(CursorProvider.VERSIONS_DIR, version, binaryName);
|
||||
if (fs.existsSync(versionPath)) {
|
||||
return versionPath;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore directory read errors
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Cursor CLI is installed
|
||||
*/
|
||||
async isInstalled(): Promise<boolean> {
|
||||
return this.cliPath !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Cursor CLI version
|
||||
*/
|
||||
async getVersion(): Promise<string | null> {
|
||||
if (!this.cliPath) return null;
|
||||
|
||||
try {
|
||||
const result = execSync(`"${this.cliPath}" --version`, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
}).trim();
|
||||
return result;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authentication status
|
||||
*/
|
||||
async checkAuth(): Promise<CursorAuthStatus> {
|
||||
if (!this.cliPath) {
|
||||
return { authenticated: false, method: 'none' };
|
||||
}
|
||||
|
||||
// Check for API key in environment
|
||||
if (process.env.CURSOR_API_KEY) {
|
||||
return { authenticated: true, method: 'api_key' };
|
||||
}
|
||||
|
||||
// Check for credentials file (location may vary)
|
||||
const credentialPaths = [
|
||||
path.join(os.homedir(), '.cursor', 'credentials.json'),
|
||||
path.join(os.homedir(), '.config', 'cursor', 'credentials.json'),
|
||||
];
|
||||
|
||||
for (const credPath of credentialPaths) {
|
||||
if (fs.existsSync(credPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(credPath, 'utf8');
|
||||
const creds = JSON.parse(content);
|
||||
if (creds.accessToken || creds.token) {
|
||||
return { authenticated: true, method: 'login', hasCredentialsFile: true };
|
||||
}
|
||||
} catch {
|
||||
// Invalid credentials file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try running a simple command to check auth
|
||||
try {
|
||||
execSync(`"${this.cliPath}" --version`, {
|
||||
encoding: 'utf8',
|
||||
timeout: 10000,
|
||||
env: { ...process.env },
|
||||
});
|
||||
// If we get here without error, assume authenticated
|
||||
// (actual auth check would need a real API call)
|
||||
return { authenticated: true, method: 'login' };
|
||||
} catch (error: any) {
|
||||
if (error.stderr?.includes('not authenticated') || error.stderr?.includes('log in')) {
|
||||
return { authenticated: false, method: 'none' };
|
||||
}
|
||||
}
|
||||
|
||||
return { authenticated: false, method: 'none' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect installation status (required by BaseProvider)
|
||||
*/
|
||||
async detectInstallation(): Promise<InstallationStatus> {
|
||||
const installed = await this.isInstalled();
|
||||
const version = installed ? await this.getVersion() : undefined;
|
||||
const auth = await this.checkAuth();
|
||||
|
||||
return {
|
||||
installed,
|
||||
version: version || undefined,
|
||||
path: this.cliPath || undefined,
|
||||
method: 'cli',
|
||||
hasApiKey: !!process.env.CURSOR_API_KEY,
|
||||
authenticated: auth.authenticated,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available Cursor models
|
||||
*/
|
||||
getAvailableModels(): ModelDefinition[] {
|
||||
return Object.entries(CURSOR_MODEL_MAP).map(([id, config]) => ({
|
||||
id: `cursor-${id}`,
|
||||
name: config.label,
|
||||
modelString: id,
|
||||
provider: 'cursor',
|
||||
description: config.description,
|
||||
tier: config.tier === 'pro' ? 'premium' : 'basic',
|
||||
supportsTools: true,
|
||||
supportsVision: false, // Cursor CLI may not support vision
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CursorError with details
|
||||
*/
|
||||
private createError(
|
||||
code: CursorErrorCode,
|
||||
message: string,
|
||||
recoverable: boolean = false,
|
||||
suggestion?: string
|
||||
): CursorError {
|
||||
const error = new Error(message) as CursorError;
|
||||
error.code = code;
|
||||
error.recoverable = recoverable;
|
||||
error.suggestion = suggestion;
|
||||
error.name = 'CursorError';
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map stderr/exit codes to detailed CursorError
|
||||
*/
|
||||
private mapError(stderr: string, exitCode: number | null): CursorError {
|
||||
const lower = stderr.toLowerCase();
|
||||
|
||||
if (
|
||||
lower.includes('not authenticated') ||
|
||||
lower.includes('please log in') ||
|
||||
lower.includes('unauthorized')
|
||||
) {
|
||||
return this.createError(
|
||||
CursorErrorCode.NOT_AUTHENTICATED,
|
||||
'Cursor CLI is not authenticated',
|
||||
true,
|
||||
'Run "cursor-agent login" to authenticate with your browser'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
lower.includes('rate limit') ||
|
||||
lower.includes('too many requests') ||
|
||||
lower.includes('429')
|
||||
) {
|
||||
return this.createError(
|
||||
CursorErrorCode.RATE_LIMITED,
|
||||
'Cursor API rate limit exceeded',
|
||||
true,
|
||||
'Wait a few minutes and try again, or upgrade to Cursor Pro'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
lower.includes('model not available') ||
|
||||
lower.includes('invalid model') ||
|
||||
lower.includes('unknown model')
|
||||
) {
|
||||
return this.createError(
|
||||
CursorErrorCode.MODEL_UNAVAILABLE,
|
||||
'Requested model is not available',
|
||||
true,
|
||||
'Try using "auto" mode or select a different model'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
lower.includes('network') ||
|
||||
lower.includes('connection') ||
|
||||
lower.includes('econnrefused') ||
|
||||
lower.includes('timeout')
|
||||
) {
|
||||
return this.createError(
|
||||
CursorErrorCode.NETWORK_ERROR,
|
||||
'Network connection error',
|
||||
true,
|
||||
'Check your internet connection and try again'
|
||||
);
|
||||
}
|
||||
|
||||
if (exitCode === 137 || lower.includes('killed') || lower.includes('sigterm')) {
|
||||
return this.createError(
|
||||
CursorErrorCode.PROCESS_CRASHED,
|
||||
'Cursor agent process was terminated',
|
||||
true,
|
||||
'The process may have run out of memory. Try a simpler task.'
|
||||
);
|
||||
}
|
||||
|
||||
return this.createError(
|
||||
CursorErrorCode.UNKNOWN,
|
||||
stderr || `Cursor agent exited with code ${exitCode}`,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a line of stream-json output
|
||||
*/
|
||||
private parseStreamLine(line: string): CursorStreamEvent | null {
|
||||
if (!line.trim()) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(line) as CursorStreamEvent;
|
||||
} catch {
|
||||
logger.debug('[CursorProvider] Failed to parse stream line:', line);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Cursor event to AutoMaker ProviderMessage format
|
||||
*/
|
||||
private normalizeEvent(event: CursorStreamEvent): ProviderMessage | null {
|
||||
switch (event.type) {
|
||||
case 'system':
|
||||
// System init - we capture session_id but don't yield a message
|
||||
return null;
|
||||
|
||||
case 'user':
|
||||
// User message - already handled by caller
|
||||
return null;
|
||||
|
||||
case 'assistant': {
|
||||
const assistantEvent = event as CursorAssistantEvent;
|
||||
return {
|
||||
type: 'assistant',
|
||||
session_id: assistantEvent.session_id,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: assistantEvent.message.content.map((c) => ({
|
||||
type: 'text' as const,
|
||||
text: c.text,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'tool_call': {
|
||||
const toolEvent = event as CursorToolCallEvent;
|
||||
const toolCall = toolEvent.tool_call;
|
||||
|
||||
// Determine tool name and input
|
||||
let toolName: string;
|
||||
let toolInput: unknown;
|
||||
|
||||
if (toolCall.readToolCall) {
|
||||
toolName = 'Read';
|
||||
toolInput = { file_path: toolCall.readToolCall.args.path };
|
||||
} else if (toolCall.writeToolCall) {
|
||||
toolName = 'Write';
|
||||
toolInput = {
|
||||
file_path: toolCall.writeToolCall.args.path,
|
||||
content: toolCall.writeToolCall.args.fileText,
|
||||
};
|
||||
} else if (toolCall.function) {
|
||||
toolName = toolCall.function.name;
|
||||
try {
|
||||
toolInput = JSON.parse(toolCall.function.arguments || '{}');
|
||||
} catch {
|
||||
toolInput = { raw: toolCall.function.arguments };
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For started events, emit tool_use
|
||||
if (toolEvent.subtype === 'started') {
|
||||
return {
|
||||
type: 'assistant',
|
||||
session_id: toolEvent.session_id,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool_use',
|
||||
name: toolName,
|
||||
tool_use_id: toolEvent.call_id,
|
||||
input: toolInput,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// For completed events, emit tool_result
|
||||
if (toolEvent.subtype === 'completed') {
|
||||
let resultContent = '';
|
||||
|
||||
if (toolCall.readToolCall?.result?.success) {
|
||||
resultContent = toolCall.readToolCall.result.success.content;
|
||||
} else if (toolCall.writeToolCall?.result?.success) {
|
||||
resultContent = `Wrote ${toolCall.writeToolCall.result.success.linesCreated} lines to ${toolCall.writeToolCall.result.success.path}`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'assistant',
|
||||
session_id: toolEvent.session_id,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool_result',
|
||||
tool_use_id: toolEvent.call_id,
|
||||
content: resultContent,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
case 'result': {
|
||||
const resultEvent = event as CursorResultEvent;
|
||||
|
||||
if (resultEvent.is_error) {
|
||||
return {
|
||||
type: 'error',
|
||||
session_id: resultEvent.session_id,
|
||||
error: resultEvent.error || resultEvent.result || 'Unknown error',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'result',
|
||||
subtype: 'success',
|
||||
session_id: resultEvent.session_id,
|
||||
result: resultEvent.result,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a prompt using Cursor CLI with streaming
|
||||
*/
|
||||
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
||||
if (!this.cliPath) {
|
||||
throw this.createError(
|
||||
CursorErrorCode.NOT_INSTALLED,
|
||||
'Cursor CLI is not installed',
|
||||
true,
|
||||
'Install with: curl https://cursor.com/install -fsS | bash'
|
||||
);
|
||||
}
|
||||
|
||||
// Extract model from options (strip 'cursor-' prefix if present)
|
||||
let model = options.model || 'auto';
|
||||
if (model.startsWith('cursor-')) {
|
||||
model = model.substring(7);
|
||||
}
|
||||
|
||||
const cwd = options.cwd || process.cwd();
|
||||
|
||||
// Build prompt content
|
||||
let promptText: string;
|
||||
if (typeof options.prompt === 'string') {
|
||||
promptText = options.prompt;
|
||||
} else if (Array.isArray(options.prompt)) {
|
||||
promptText = options.prompt
|
||||
.filter((p) => p.type === 'text' && p.text)
|
||||
.map((p) => p.text)
|
||||
.join('\n');
|
||||
} else {
|
||||
throw new Error('Invalid prompt format');
|
||||
}
|
||||
|
||||
// Build CLI arguments
|
||||
const args: string[] = [
|
||||
'-p', // Print mode (non-interactive)
|
||||
'--force', // Allow file modifications
|
||||
'--output-format',
|
||||
'stream-json',
|
||||
'--stream-partial-output', // Real-time streaming
|
||||
];
|
||||
|
||||
// Add model if not auto
|
||||
if (model !== 'auto') {
|
||||
args.push('--model', model);
|
||||
}
|
||||
|
||||
// Add the prompt
|
||||
args.push(promptText);
|
||||
|
||||
logger.debug(`[CursorProvider] Executing: ${this.cliPath} ${args.slice(0, 6).join(' ')}...`);
|
||||
|
||||
// Use spawnJSONLProcess from @automaker/platform for JSONL streaming
|
||||
// This handles line buffering, timeouts, and abort signals automatically
|
||||
const subprocessOptions: SubprocessOptions = {
|
||||
command: this.cliPath,
|
||||
args,
|
||||
cwd,
|
||||
env: { ...process.env },
|
||||
abortController: options.abortController,
|
||||
timeout: 120000, // 2 min timeout for CLI operations (may take longer than default 30s)
|
||||
};
|
||||
|
||||
let sessionId: string | undefined;
|
||||
|
||||
try {
|
||||
// spawnJSONLProcess yields parsed JSON objects, handles errors
|
||||
for await (const rawEvent of spawnJSONLProcess(subprocessOptions)) {
|
||||
const event = rawEvent as CursorStreamEvent;
|
||||
|
||||
// Capture session ID from system init
|
||||
if (event.type === 'system' && (event as CursorSystemEvent).subtype === 'init') {
|
||||
sessionId = event.session_id;
|
||||
}
|
||||
|
||||
// Normalize and yield the event
|
||||
const normalized = this.normalizeEvent(event);
|
||||
if (normalized) {
|
||||
// Ensure session_id is always set
|
||||
if (!normalized.session_id && sessionId) {
|
||||
normalized.session_id = sessionId;
|
||||
}
|
||||
yield normalized;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Use isAbortError from @automaker/utils for abort detection
|
||||
if (isAbortError(error)) {
|
||||
return; // Clean abort, don't throw
|
||||
}
|
||||
|
||||
// Map CLI errors to CursorError
|
||||
if (error instanceof Error && 'stderr' in error) {
|
||||
throw this.mapError((error as any).stderr || error.message, (error as any).exitCode);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the current execution
|
||||
*/
|
||||
abort(): void {
|
||||
if (this.currentProcess) {
|
||||
this.currentProcess.kill('SIGTERM');
|
||||
this.currentProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature is supported
|
||||
*/
|
||||
supportsFeature(feature: string): boolean {
|
||||
const supported = ['tools', 'text', 'streaming'];
|
||||
return supported.includes(feature);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task 2.2: Create Cursor Config Manager
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/server/src/providers/cursor-config-manager.ts`
|
||||
|
||||
```typescript
|
||||
import * as path from 'path';
|
||||
import { CursorCliConfig, CursorModelId } from '@automaker/types';
|
||||
import { createLogger, mkdirSafe, existsSafe } from '@automaker/utils';
|
||||
import { getAutomakerDir } from '@automaker/platform';
|
||||
import { secureFs } from '@automaker/platform';
|
||||
|
||||
// Create logger for this module
|
||||
const logger = createLogger('CursorConfigManager');
|
||||
|
||||
/**
|
||||
* Manages Cursor CLI configuration
|
||||
* Config location: .automaker/cursor-config.json
|
||||
*/
|
||||
export class CursorConfigManager {
|
||||
private configPath: string;
|
||||
private config: CursorCliConfig;
|
||||
|
||||
constructor(projectPath: string) {
|
||||
// Use getAutomakerDir for consistent path resolution
|
||||
this.configPath = path.join(getAutomakerDir(projectPath), 'cursor-config.json');
|
||||
this.config = this.loadConfig();
|
||||
}
|
||||
|
||||
private loadConfig(): CursorCliConfig {
|
||||
try {
|
||||
if (fs.existsSync(this.configPath)) {
|
||||
const content = fs.readFileSync(this.configPath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[CursorConfigManager] Failed to load config:', error);
|
||||
}
|
||||
|
||||
// Return default config
|
||||
return {
|
||||
defaultModel: 'auto',
|
||||
models: ['auto', 'claude-sonnet-4', 'gpt-4o-mini'],
|
||||
};
|
||||
}
|
||||
|
||||
private saveConfig(): void {
|
||||
try {
|
||||
const dir = path.dirname(this.configPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
|
||||
logger.debug('[CursorConfigManager] Config saved');
|
||||
} catch (error) {
|
||||
logger.error('[CursorConfigManager] Failed to save config:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getConfig(): CursorCliConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
getDefaultModel(): CursorModelId {
|
||||
return this.config.defaultModel || 'auto';
|
||||
}
|
||||
|
||||
setDefaultModel(model: CursorModelId): void {
|
||||
this.config.defaultModel = model;
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
getEnabledModels(): CursorModelId[] {
|
||||
return this.config.models || ['auto'];
|
||||
}
|
||||
|
||||
setEnabledModels(models: CursorModelId[]): void {
|
||||
this.config.models = models;
|
||||
this.saveConfig();
|
||||
}
|
||||
|
||||
addModel(model: CursorModelId): void {
|
||||
if (!this.config.models) {
|
||||
this.config.models = [];
|
||||
}
|
||||
if (!this.config.models.includes(model)) {
|
||||
this.config.models.push(model);
|
||||
this.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
removeModel(model: CursorModelId): void {
|
||||
if (this.config.models) {
|
||||
this.config.models = this.config.models.filter((m) => m !== model);
|
||||
this.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Provider Instantiation
|
||||
|
||||
```typescript
|
||||
// test-cursor-provider.ts
|
||||
import { CursorProvider } from './apps/server/src/providers/cursor-provider';
|
||||
|
||||
const provider = new CursorProvider();
|
||||
console.log('Provider name:', provider.getName()); // Should be 'cursor'
|
||||
|
||||
const status = await provider.detectInstallation();
|
||||
console.log('Installation status:', status);
|
||||
|
||||
const models = provider.getAvailableModels();
|
||||
console.log('Available models:', models.length);
|
||||
```
|
||||
|
||||
### Test 2: CLI Detection (requires cursor-agent installed)
|
||||
|
||||
```bash
|
||||
# Check if cursor-agent is found
|
||||
node -e "
|
||||
const { CursorProvider } = require('./apps/server/dist/providers/cursor-provider');
|
||||
const p = new CursorProvider();
|
||||
p.isInstalled().then(installed => {
|
||||
console.log('Installed:', installed);
|
||||
if (installed) {
|
||||
p.getVersion().then(v => console.log('Version:', v));
|
||||
p.checkAuth().then(a => console.log('Auth:', a));
|
||||
}
|
||||
});
|
||||
"
|
||||
```
|
||||
|
||||
### Test 3: Simple Query (requires cursor-agent authenticated)
|
||||
|
||||
```typescript
|
||||
// test-cursor-query.ts
|
||||
import { CursorProvider } from './apps/server/src/providers/cursor-provider';
|
||||
|
||||
const provider = new CursorProvider();
|
||||
const stream = provider.executeQuery({
|
||||
prompt: 'What is 2 + 2? Reply with just the number.',
|
||||
model: 'auto',
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
for await (const msg of stream) {
|
||||
console.log('Message:', JSON.stringify(msg, null, 2));
|
||||
}
|
||||
```
|
||||
|
||||
### Test 4: Error Handling
|
||||
|
||||
```typescript
|
||||
// Test with invalid model
|
||||
try {
|
||||
const stream = provider.executeQuery({
|
||||
prompt: 'test',
|
||||
model: 'invalid-model-xyz',
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
for await (const msg of stream) {
|
||||
// Should not reach here
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error code:', error.code);
|
||||
console.log('Suggestion:', error.suggestion);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [ ] `cursor-provider.ts` compiles without errors
|
||||
- [ ] `cursor-config-manager.ts` compiles without errors
|
||||
- [ ] Provider returns correct name ('cursor')
|
||||
- [ ] `detectInstallation()` correctly detects CLI
|
||||
- [ ] `getAvailableModels()` returns model definitions
|
||||
- [ ] `executeQuery()` streams messages (if CLI installed)
|
||||
- [ ] Errors are properly mapped to CursorError
|
||||
- [ ] Abort signal terminates process
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| ---------------------------------------------------- | ------ | ----------------- |
|
||||
| `apps/server/src/providers/cursor-provider.ts` | Create | Main provider |
|
||||
| `apps/server/src/providers/cursor-config-manager.ts` | Create | Config management |
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Windows Support**: CLI path detection may need adjustment
|
||||
2. **Vision**: Cursor CLI may not support image inputs
|
||||
3. **Resume**: Session resumption not implemented in Phase 2
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The provider uses `--stream-partial-output` for real-time character streaming
|
||||
- Tool call events are normalized to match Claude SDK format
|
||||
- Session IDs are captured from system init event
|
||||
@@ -1,229 +0,0 @@
|
||||
# Phase 3: Provider Factory Integration
|
||||
|
||||
**Status:** `completed`
|
||||
**Dependencies:** Phase 2 (Provider)
|
||||
**Estimated Effort:** Small (routing logic only)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Integrate CursorProvider into the ProviderFactory so models are automatically routed to the correct provider.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 3.1: Update Provider Factory
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/server/src/providers/provider-factory.ts`
|
||||
|
||||
Add Cursor provider import and routing:
|
||||
|
||||
```typescript
|
||||
import { CursorProvider } from './cursor-provider';
|
||||
import { CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
|
||||
export class ProviderFactory {
|
||||
/**
|
||||
* Determine which provider to use for a given model
|
||||
*/
|
||||
static getProviderNameForModel(model: string): 'claude' | 'cursor' {
|
||||
const lowerModel = model.toLowerCase();
|
||||
|
||||
// Check for explicit cursor prefix
|
||||
if (lowerModel.startsWith('cursor-')) {
|
||||
return 'cursor';
|
||||
}
|
||||
|
||||
// Check if it's a known Cursor model ID
|
||||
const cursorModelId = lowerModel.replace('cursor-', '');
|
||||
if (cursorModelId in CURSOR_MODEL_MAP) {
|
||||
return 'cursor';
|
||||
}
|
||||
|
||||
// Check for Cursor-specific patterns
|
||||
if (
|
||||
lowerModel === 'auto' ||
|
||||
lowerModel.includes('gpt-') ||
|
||||
lowerModel.includes('gemini-') ||
|
||||
lowerModel === 'o3-mini'
|
||||
) {
|
||||
// These could be Cursor models, but we default to Claude
|
||||
// unless explicitly prefixed with cursor-
|
||||
}
|
||||
|
||||
// Check for Claude model patterns
|
||||
if (
|
||||
lowerModel.startsWith('claude-') ||
|
||||
['opus', 'sonnet', 'haiku'].some((n) => lowerModel.includes(n))
|
||||
) {
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
// Default to Claude
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a provider instance for the given model
|
||||
*/
|
||||
static getProviderForModel(model: string, config?: ProviderConfig): BaseProvider {
|
||||
const providerName = this.getProviderNameForModel(model);
|
||||
|
||||
if (providerName === 'cursor') {
|
||||
return new CursorProvider(config);
|
||||
}
|
||||
|
||||
return new ClaudeProvider(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered providers
|
||||
*/
|
||||
static getAllProviders(): BaseProvider[] {
|
||||
return [new ClaudeProvider(), new CursorProvider()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a provider by name
|
||||
*/
|
||||
static getProviderByName(name: string): BaseProvider | null {
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
switch (lowerName) {
|
||||
case 'claude':
|
||||
return new ClaudeProvider();
|
||||
case 'cursor':
|
||||
return new CursorProvider();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check installation status of all providers
|
||||
*/
|
||||
static async checkAllProviders(): Promise<Record<string, InstallationStatus>> {
|
||||
const providers = this.getAllProviders();
|
||||
const statuses: Record<string, InstallationStatus> = {};
|
||||
|
||||
await Promise.all(
|
||||
providers.map(async (provider) => {
|
||||
const status = await provider.detectInstallation();
|
||||
statuses[provider.getName()] = status;
|
||||
})
|
||||
);
|
||||
|
||||
return statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available models from all providers
|
||||
*/
|
||||
static getAllAvailableModels(): ModelDefinition[] {
|
||||
const providers = this.getAllProviders();
|
||||
return providers.flatMap((p) => p.getAvailableModels());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task 3.2: Export CursorProvider
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/server/src/providers/index.ts`
|
||||
|
||||
Add export:
|
||||
|
||||
```typescript
|
||||
export { CursorProvider, CursorErrorCode, CursorError } from './cursor-provider';
|
||||
export { CursorConfigManager } from './cursor-config-manager';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Model Routing
|
||||
|
||||
```typescript
|
||||
import { ProviderFactory } from './apps/server/src/providers/provider-factory';
|
||||
|
||||
// Cursor models
|
||||
console.assert(ProviderFactory.getProviderNameForModel('cursor-auto') === 'cursor');
|
||||
console.assert(ProviderFactory.getProviderNameForModel('cursor-gpt-4o') === 'cursor');
|
||||
console.assert(ProviderFactory.getProviderNameForModel('cursor-claude-sonnet-4') === 'cursor');
|
||||
|
||||
// Claude models (default)
|
||||
console.assert(ProviderFactory.getProviderNameForModel('claude-sonnet-4') === 'claude');
|
||||
console.assert(ProviderFactory.getProviderNameForModel('opus') === 'claude');
|
||||
console.assert(ProviderFactory.getProviderNameForModel('sonnet') === 'claude');
|
||||
console.assert(ProviderFactory.getProviderNameForModel('haiku') === 'claude');
|
||||
|
||||
// Unknown models default to Claude
|
||||
console.assert(ProviderFactory.getProviderNameForModel('unknown-model') === 'claude');
|
||||
|
||||
console.log('All routing tests passed!');
|
||||
```
|
||||
|
||||
### Test 2: Provider Instantiation
|
||||
|
||||
```typescript
|
||||
import { ProviderFactory } from './apps/server/src/providers/provider-factory';
|
||||
|
||||
const cursorProvider = ProviderFactory.getProviderForModel('cursor-auto');
|
||||
console.assert(cursorProvider.getName() === 'cursor');
|
||||
|
||||
const claudeProvider = ProviderFactory.getProviderForModel('sonnet');
|
||||
console.assert(claudeProvider.getName() === 'claude');
|
||||
|
||||
console.log('Provider instantiation tests passed!');
|
||||
```
|
||||
|
||||
### Test 3: All Providers Check
|
||||
|
||||
```typescript
|
||||
import { ProviderFactory } from './apps/server/src/providers/provider-factory';
|
||||
|
||||
const statuses = await ProviderFactory.checkAllProviders();
|
||||
console.log('Provider statuses:', statuses);
|
||||
// Should have both 'claude' and 'cursor' keys
|
||||
|
||||
const allModels = ProviderFactory.getAllAvailableModels();
|
||||
console.log('Total models:', allModels.length);
|
||||
// Should include models from both providers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [x] ProviderFactory routes `cursor-*` models to CursorProvider
|
||||
- [x] ProviderFactory routes Claude models to ClaudeProvider
|
||||
- [x] `getAllProviders()` returns both providers
|
||||
- [x] `getProviderByName('cursor')` returns CursorProvider
|
||||
- [x] `checkAllProviders()` returns status for both providers
|
||||
- [x] `getAllAvailableModels()` includes Cursor models
|
||||
- [x] Existing Claude routing not broken
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| ----------------------------------------------- | ------ | --------------------- |
|
||||
| `apps/server/src/providers/provider-factory.ts` | Modify | Add Cursor routing |
|
||||
| `apps/server/src/providers/index.ts` | Modify | Export CursorProvider |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Model routing uses prefix matching for explicit `cursor-` models
|
||||
- Unknown models default to Claude for backward compatibility
|
||||
- The factory is stateless - new provider instances created per call
|
||||
@@ -1,348 +0,0 @@
|
||||
# Phase 4: Setup Routes & Status Endpoints
|
||||
|
||||
**Status:** `completed`
|
||||
**Dependencies:** Phase 3 (Factory)
|
||||
**Estimated Effort:** Medium (API endpoints)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Create API endpoints for checking Cursor CLI status and managing configuration.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 4.1: Create Cursor Status Route
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/server/src/routes/setup/routes/cursor-status.ts`
|
||||
|
||||
```typescript
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { CursorProvider } from '../../../providers/cursor-provider';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
|
||||
// Create logger for this module
|
||||
const logger = createLogger('CursorStatusRoute');
|
||||
|
||||
/**
|
||||
* GET /api/setup/cursor-status
|
||||
* Returns Cursor CLI installation and authentication status
|
||||
*/
|
||||
export function createCursorStatusHandler() {
|
||||
return async (req: Request, res: Response) => {
|
||||
try {
|
||||
const provider = new CursorProvider();
|
||||
|
||||
const [installed, version, auth] = await Promise.all([
|
||||
provider.isInstalled(),
|
||||
provider.getVersion(),
|
||||
provider.checkAuth(),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
installed,
|
||||
version: version || null,
|
||||
path: installed ? (provider as any).cliPath : null,
|
||||
auth: {
|
||||
authenticated: auth.authenticated,
|
||||
method: auth.method,
|
||||
},
|
||||
installCommand: 'curl https://cursor.com/install -fsS | bash',
|
||||
loginCommand: 'cursor-agent login',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[cursor-status] Error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createCursorStatusRoute(): Router {
|
||||
const router = Router();
|
||||
router.get('/cursor-status', createCursorStatusHandler());
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
### Task 4.2: Create Cursor Config Routes
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/server/src/routes/setup/routes/cursor-config.ts`
|
||||
|
||||
```typescript
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { CursorConfigManager } from '../../../providers/cursor-config-manager';
|
||||
import { CURSOR_MODEL_MAP, CursorModelId } from '@automaker/types';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
|
||||
// Create logger for this module
|
||||
const logger = createLogger('CursorConfigRoute');
|
||||
|
||||
export function createCursorConfigRoutes(dataDir: string): Router {
|
||||
const router = Router();
|
||||
const configManager = new CursorConfigManager(dataDir);
|
||||
|
||||
/**
|
||||
* GET /api/setup/cursor-config
|
||||
* Get current Cursor configuration
|
||||
*/
|
||||
router.get('/cursor-config', (req: Request, res: Response) => {
|
||||
try {
|
||||
res.json({
|
||||
success: true,
|
||||
config: configManager.getConfig(),
|
||||
availableModels: Object.values(CURSOR_MODEL_MAP),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[cursor-config] GET error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/setup/cursor-config/default-model
|
||||
* Set the default Cursor model
|
||||
*/
|
||||
router.post('/cursor-config/default-model', (req: Request, res: Response) => {
|
||||
try {
|
||||
const { model } = req.body;
|
||||
|
||||
if (!model || !(model in CURSOR_MODEL_MAP)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid model ID. Valid models: ${Object.keys(CURSOR_MODEL_MAP).join(', ')}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
configManager.setDefaultModel(model as CursorModelId);
|
||||
res.json({ success: true, model });
|
||||
} catch (error) {
|
||||
logger.error('[cursor-config] POST default-model error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/setup/cursor-config/models
|
||||
* Set enabled Cursor models
|
||||
*/
|
||||
router.post('/cursor-config/models', (req: Request, res: Response) => {
|
||||
try {
|
||||
const { models } = req.body;
|
||||
|
||||
if (!Array.isArray(models)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Models must be an array',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter to valid models only
|
||||
const validModels = models.filter((m): m is CursorModelId => m in CURSOR_MODEL_MAP);
|
||||
|
||||
if (validModels.length === 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'No valid models provided',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
configManager.setEnabledModels(validModels);
|
||||
res.json({ success: true, models: validModels });
|
||||
} catch (error) {
|
||||
logger.error('[cursor-config] POST models error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
### Task 4.3: Register Routes in Setup Index
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/server/src/routes/setup/index.ts`
|
||||
|
||||
Add to existing router:
|
||||
|
||||
```typescript
|
||||
import { createCursorStatusRoute } from './routes/cursor-status';
|
||||
import { createCursorConfigRoutes } from './routes/cursor-config';
|
||||
|
||||
// In the router setup function:
|
||||
export function createSetupRouter(dataDir: string): Router {
|
||||
const router = Router();
|
||||
|
||||
// Existing routes...
|
||||
router.get('/claude-status', createClaudeStatusHandler());
|
||||
// ...
|
||||
|
||||
// Add Cursor routes
|
||||
router.use(createCursorStatusRoute());
|
||||
router.use(createCursorConfigRoutes(dataDir));
|
||||
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
### Task 4.4: Update HttpApiClient
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/lib/http-api-client.ts`
|
||||
|
||||
Add Cursor methods to the HttpApiClient setup object:
|
||||
|
||||
```typescript
|
||||
// In HttpApiClient class, extend the setup object:
|
||||
|
||||
setup = {
|
||||
// Existing methods...
|
||||
getClaudeStatus: () => this.get('/api/setup/claude-status'),
|
||||
|
||||
// Add Cursor methods
|
||||
getCursorStatus: () =>
|
||||
this.get<{
|
||||
success: boolean;
|
||||
installed?: boolean;
|
||||
version?: string;
|
||||
path?: string;
|
||||
auth?: {
|
||||
authenticated: boolean;
|
||||
method: string;
|
||||
};
|
||||
installCommand?: string;
|
||||
loginCommand?: string;
|
||||
error?: string;
|
||||
}>('/api/setup/cursor-status'),
|
||||
|
||||
getCursorConfig: () =>
|
||||
this.get<{
|
||||
success: boolean;
|
||||
config?: CursorCliConfig;
|
||||
availableModels?: CursorModelConfig[];
|
||||
error?: string;
|
||||
}>('/api/setup/cursor-config'),
|
||||
|
||||
setCursorDefaultModel: (model: CursorModelId) =>
|
||||
this.post<{ success: boolean; error?: string }>('/api/setup/cursor-config/default-model', {
|
||||
model,
|
||||
}),
|
||||
|
||||
setCursorModels: (models: CursorModelId[]) =>
|
||||
this.post<{ success: boolean; error?: string }>('/api/setup/cursor-config/models', { models }),
|
||||
};
|
||||
```
|
||||
|
||||
This integrates with the existing HttpApiClient pattern used throughout the UI.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Status Endpoint
|
||||
|
||||
```bash
|
||||
# Start the server, then:
|
||||
curl http://localhost:3001/api/setup/cursor-status
|
||||
|
||||
# Expected response (if installed):
|
||||
# {
|
||||
# "success": true,
|
||||
# "installed": true,
|
||||
# "version": "0.1.0",
|
||||
# "path": "/home/user/.local/bin/cursor-agent",
|
||||
# "auth": { "authenticated": true, "method": "login" }
|
||||
# }
|
||||
|
||||
# Expected response (if not installed):
|
||||
# {
|
||||
# "success": true,
|
||||
# "installed": false,
|
||||
# "installCommand": "curl https://cursor.com/install -fsS | bash"
|
||||
# }
|
||||
```
|
||||
|
||||
### Test 2: Config Endpoints
|
||||
|
||||
```bash
|
||||
# Get config
|
||||
curl http://localhost:3001/api/setup/cursor-config
|
||||
|
||||
# Set default model
|
||||
curl -X POST http://localhost:3001/api/setup/cursor-config/default-model \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "gpt-4o"}'
|
||||
|
||||
# Set enabled models
|
||||
curl -X POST http://localhost:3001/api/setup/cursor-config/models \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"models": ["auto", "gpt-4o", "claude-sonnet-4"]}'
|
||||
```
|
||||
|
||||
### Test 3: Error Handling
|
||||
|
||||
```bash
|
||||
# Invalid model should return 400
|
||||
curl -X POST http://localhost:3001/api/setup/cursor-config/default-model \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model": "invalid-model"}'
|
||||
|
||||
# Expected: {"success": false, "error": "Invalid model ID..."}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [x] `/api/setup/cursor-status` returns installation status
|
||||
- [x] `/api/setup/cursor-config` returns current config
|
||||
- [x] `/api/setup/cursor-config/default-model` updates default
|
||||
- [x] `/api/setup/cursor-config/models` updates enabled models
|
||||
- [x] Error responses have correct status codes (400, 500)
|
||||
- [x] Config persists to file after changes
|
||||
- [x] HttpApiClient updated with Cursor methods (using web mode, not Electron IPC)
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| ------------------------------------------------------ | ------ | ----------------------------- |
|
||||
| `apps/server/src/routes/setup/routes/cursor-status.ts` | Create | Status endpoint |
|
||||
| `apps/server/src/routes/setup/routes/cursor-config.ts` | Create | Config endpoints |
|
||||
| `apps/server/src/routes/setup/index.ts` | Modify | Register routes |
|
||||
| `apps/ui/src/lib/http-api-client.ts` | Modify | Add Cursor API client methods |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Config is stored in `.automaker/cursor-config.json`
|
||||
- The status endpoint is optimized for quick checks (parallel calls)
|
||||
- Install/login commands are included in response for UI display
|
||||
@@ -1,374 +0,0 @@
|
||||
# Phase 5: Log Parser Integration
|
||||
|
||||
**Status:** `completed`
|
||||
**Dependencies:** Phase 2 (Provider), Phase 3 (Factory)
|
||||
**Estimated Effort:** Small (parser extension)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Update the log parser to recognize and normalize Cursor CLI stream events for display in the log viewer.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 5.1: Add Cursor Event Type Detection
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/lib/log-parser.ts`
|
||||
|
||||
Add Cursor event detection and normalization:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
CursorStreamEvent,
|
||||
CursorSystemEvent,
|
||||
CursorAssistantEvent,
|
||||
CursorToolCallEvent,
|
||||
CursorResultEvent,
|
||||
} from '@automaker/types';
|
||||
|
||||
/**
|
||||
* Detect if a parsed JSON object is a Cursor stream event
|
||||
*/
|
||||
function isCursorEvent(obj: any): obj is CursorStreamEvent {
|
||||
return (
|
||||
obj &&
|
||||
typeof obj === 'object' &&
|
||||
'type' in obj &&
|
||||
'session_id' in obj &&
|
||||
['system', 'user', 'assistant', 'tool_call', 'result'].includes(obj.type)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize Cursor stream event to log entry
|
||||
*/
|
||||
export function normalizeCursorEvent(event: CursorStreamEvent): LogEntry | null {
|
||||
const timestamp = new Date().toISOString();
|
||||
const baseEntry = {
|
||||
id: `cursor-${event.session_id}-${Date.now()}`,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
switch (event.type) {
|
||||
case 'system': {
|
||||
const sysEvent = event as CursorSystemEvent;
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'info' as LogEntryType,
|
||||
title: 'Session Started',
|
||||
content: `Model: ${sysEvent.model}\nAuth: ${sysEvent.apiKeySource}\nCWD: ${sysEvent.cwd}`,
|
||||
collapsed: true,
|
||||
metadata: {
|
||||
phase: 'init',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'assistant': {
|
||||
const assistEvent = event as CursorAssistantEvent;
|
||||
const text = assistEvent.message.content
|
||||
.filter((c) => c.type === 'text')
|
||||
.map((c) => c.text)
|
||||
.join('');
|
||||
|
||||
if (!text.trim()) return null;
|
||||
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'info' as LogEntryType,
|
||||
title: 'Assistant',
|
||||
content: text,
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
|
||||
case 'tool_call': {
|
||||
const toolEvent = event as CursorToolCallEvent;
|
||||
return normalizeCursorToolCall(toolEvent, baseEntry);
|
||||
}
|
||||
|
||||
case 'result': {
|
||||
const resultEvent = event as CursorResultEvent;
|
||||
|
||||
if (resultEvent.is_error) {
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'error' as LogEntryType,
|
||||
title: 'Error',
|
||||
content: resultEvent.error || resultEvent.result || 'Unknown error',
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseEntry,
|
||||
type: 'success' as LogEntryType,
|
||||
title: 'Completed',
|
||||
content: `Duration: ${resultEvent.duration_ms}ms`,
|
||||
collapsed: true,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize Cursor tool call event
|
||||
*/
|
||||
function normalizeCursorToolCall(
|
||||
event: CursorToolCallEvent,
|
||||
baseEntry: { id: string; timestamp: string }
|
||||
): LogEntry | null {
|
||||
const toolCall = event.tool_call;
|
||||
const isStarted = event.subtype === 'started';
|
||||
const isCompleted = event.subtype === 'completed';
|
||||
|
||||
// Read tool
|
||||
if (toolCall.readToolCall) {
|
||||
const path = toolCall.readToolCall.args.path;
|
||||
const result = toolCall.readToolCall.result?.success;
|
||||
|
||||
return {
|
||||
...baseEntry,
|
||||
id: `${baseEntry.id}-${event.call_id}`,
|
||||
type: 'tool_call' as LogEntryType,
|
||||
title: isStarted ? `Reading ${path}` : `Read ${path}`,
|
||||
content:
|
||||
isCompleted && result
|
||||
? `${result.totalLines} lines, ${result.totalChars} chars`
|
||||
: `Path: ${path}`,
|
||||
collapsed: true,
|
||||
metadata: {
|
||||
toolName: 'Read',
|
||||
toolCategory: 'read' as ToolCategory,
|
||||
filePath: path,
|
||||
summary: isCompleted ? `Read ${result?.totalLines || 0} lines` : `Reading file...`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Write tool
|
||||
if (toolCall.writeToolCall) {
|
||||
const path =
|
||||
toolCall.writeToolCall.args?.path ||
|
||||
toolCall.writeToolCall.result?.success?.path ||
|
||||
'unknown';
|
||||
const result = toolCall.writeToolCall.result?.success;
|
||||
|
||||
return {
|
||||
...baseEntry,
|
||||
id: `${baseEntry.id}-${event.call_id}`,
|
||||
type: 'tool_call' as LogEntryType,
|
||||
title: isStarted ? `Writing ${path}` : `Wrote ${path}`,
|
||||
content:
|
||||
isCompleted && result
|
||||
? `${result.linesCreated} lines, ${result.fileSize} bytes`
|
||||
: `Path: ${path}`,
|
||||
collapsed: true,
|
||||
metadata: {
|
||||
toolName: 'Write',
|
||||
toolCategory: 'write' as ToolCategory,
|
||||
filePath: path,
|
||||
summary: isCompleted ? `Wrote ${result?.linesCreated || 0} lines` : `Writing file...`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Generic function tool
|
||||
if (toolCall.function) {
|
||||
const name = toolCall.function.name;
|
||||
const args = toolCall.function.arguments;
|
||||
|
||||
// Determine category based on tool name
|
||||
let category: ToolCategory = 'other';
|
||||
if (['Read', 'Glob'].includes(name)) category = 'read';
|
||||
if (['Write', 'Edit'].includes(name)) category = 'edit';
|
||||
if (['Bash'].includes(name)) category = 'bash';
|
||||
if (['Grep'].includes(name)) category = 'search';
|
||||
if (['TodoWrite'].includes(name)) category = 'todo';
|
||||
if (['Task'].includes(name)) category = 'task';
|
||||
|
||||
return {
|
||||
...baseEntry,
|
||||
id: `${baseEntry.id}-${event.call_id}`,
|
||||
type: 'tool_call' as LogEntryType,
|
||||
title: `${name} ${isStarted ? 'started' : 'completed'}`,
|
||||
content: args || '',
|
||||
collapsed: true,
|
||||
metadata: {
|
||||
toolName: name,
|
||||
toolCategory: category,
|
||||
summary: `${name} ${event.subtype}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### Task 5.2: Update parseLogLine Function
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/lib/log-parser.ts`
|
||||
|
||||
Update the main parsing function to detect Cursor events:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Parse a single log line into a structured entry
|
||||
*/
|
||||
export function parseLogLine(line: string): LogEntry | null {
|
||||
if (!line.trim()) return null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
|
||||
// Check if it's a Cursor stream event
|
||||
if (isCursorEvent(parsed)) {
|
||||
return normalizeCursorEvent(parsed);
|
||||
}
|
||||
|
||||
// Existing AutoMaker/Claude event parsing...
|
||||
return parseAutoMakerEvent(parsed);
|
||||
} catch {
|
||||
// Non-JSON line - treat as plain text
|
||||
return {
|
||||
id: `text-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
type: 'info',
|
||||
title: 'Output',
|
||||
content: line,
|
||||
timestamp: new Date().toISOString(),
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task 5.3: Add Cursor-Specific Styling (Optional)
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/lib/log-parser.ts`
|
||||
|
||||
Add provider-aware styling:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Get provider-specific styling for log entries
|
||||
*/
|
||||
export function getProviderStyle(entry: LogEntry): { badge?: string; icon?: string } {
|
||||
// Check if entry has Cursor session ID pattern
|
||||
if (entry.id.startsWith('cursor-')) {
|
||||
return {
|
||||
badge: 'Cursor',
|
||||
icon: 'terminal', // Or a Cursor-specific icon
|
||||
};
|
||||
}
|
||||
|
||||
// Default (Claude)
|
||||
return {
|
||||
badge: 'Claude',
|
||||
icon: 'bot',
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Cursor Event Parsing
|
||||
|
||||
```typescript
|
||||
import { parseLogLine, normalizeCursorEvent } from './apps/ui/src/lib/log-parser';
|
||||
|
||||
// Test system init
|
||||
const systemEvent =
|
||||
'{"type":"system","subtype":"init","apiKeySource":"login","cwd":"/project","session_id":"abc-123","model":"Claude 4 Sonnet","permissionMode":"default"}';
|
||||
const systemEntry = parseLogLine(systemEvent);
|
||||
console.assert(systemEntry?.type === 'info', 'System event should be info type');
|
||||
console.assert(systemEntry?.title === 'Session Started', 'System should have correct title');
|
||||
|
||||
// Test assistant message
|
||||
const assistantEvent =
|
||||
'{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Hello world"}]},"session_id":"abc-123"}';
|
||||
const assistantEntry = parseLogLine(assistantEvent);
|
||||
console.assert(assistantEntry?.content === 'Hello world', 'Assistant content should match');
|
||||
|
||||
// Test tool call
|
||||
const toolEvent =
|
||||
'{"type":"tool_call","subtype":"started","call_id":"call-1","tool_call":{"readToolCall":{"args":{"path":"test.ts"}}},"session_id":"abc-123"}';
|
||||
const toolEntry = parseLogLine(toolEvent);
|
||||
console.assert(toolEntry?.metadata?.toolName === 'Read', 'Tool name should be Read');
|
||||
console.assert(toolEntry?.metadata?.toolCategory === 'read', 'Category should be read');
|
||||
|
||||
console.log('All Cursor parsing tests passed!');
|
||||
```
|
||||
|
||||
### Test 2: Mixed Event Stream
|
||||
|
||||
```typescript
|
||||
// Simulate a stream with both Claude and Cursor events
|
||||
const events = [
|
||||
// Cursor events
|
||||
'{"type":"system","subtype":"init","session_id":"cur-1","model":"GPT-4o","apiKeySource":"login","cwd":"/project","permissionMode":"default"}',
|
||||
'{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Reading file..."}]},"session_id":"cur-1"}',
|
||||
'{"type":"tool_call","subtype":"started","call_id":"t1","tool_call":{"readToolCall":{"args":{"path":"README.md"}}},"session_id":"cur-1"}',
|
||||
// Claude-style event (existing format)
|
||||
'{"type":"assistant","content":[{"type":"text","text":"From Claude"}]}',
|
||||
];
|
||||
|
||||
const entries = events.map(parseLogLine).filter(Boolean);
|
||||
console.log('Parsed entries:', entries.length);
|
||||
// Should parse all events correctly
|
||||
```
|
||||
|
||||
### Test 3: Log Viewer Integration
|
||||
|
||||
1. Start the app with a Cursor provider task
|
||||
2. Observe log viewer updates in real-time
|
||||
3. Verify:
|
||||
- Tool calls show correct icons
|
||||
- File paths are highlighted
|
||||
- Collapsed by default where appropriate
|
||||
- Timestamps are displayed
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [x] `isCursorEvent()` correctly identifies Cursor events
|
||||
- [x] `normalizeCursorEvent()` handles all event types
|
||||
- [x] Tool calls are categorized correctly
|
||||
- [x] File paths extracted for Read/Write tools
|
||||
- [x] Existing Claude event parsing not broken
|
||||
- [x] Log viewer displays Cursor events correctly
|
||||
- [x] No runtime errors with malformed events
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| ------------------------------- | ------ | ------------------------------ |
|
||||
| `apps/ui/src/lib/log-parser.ts` | Modify | Add Cursor event normalization |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Cursor events have `session_id` on all events (unlike Claude SDK)
|
||||
- Tool call events come in pairs: started + completed
|
||||
- The `call_id` is used to correlate started/completed events
|
||||
- Entry IDs include session_id for uniqueness
|
||||
@@ -1,457 +0,0 @@
|
||||
# Phase 6: UI Setup Wizard
|
||||
|
||||
**Status:** `completed`
|
||||
**Dependencies:** Phase 4 (Routes)
|
||||
**Estimated Effort:** Medium (React component)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Add an optional Cursor CLI setup step to the welcome wizard, allowing users to configure Cursor as an AI provider during initial setup.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 6.1: Create Cursor Setup Step Component
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx`
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { CheckCircle2, XCircle, Loader2, ExternalLink, Terminal, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/lib/http-api-client';
|
||||
|
||||
interface CursorSetupStepProps {
|
||||
onComplete: () => void;
|
||||
onSkip: () => void;
|
||||
}
|
||||
|
||||
interface CliStatus {
|
||||
installed: boolean;
|
||||
version?: string;
|
||||
path?: string;
|
||||
auth?: {
|
||||
authenticated: boolean;
|
||||
method: string;
|
||||
};
|
||||
installCommand?: string;
|
||||
loginCommand?: string;
|
||||
}
|
||||
|
||||
export function CursorSetupStep({ onComplete, onSkip }: CursorSetupStepProps) {
|
||||
const [status, setStatus] = useState<CliStatus | null>(null);
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
|
||||
const checkStatus = useCallback(async () => {
|
||||
setIsChecking(true);
|
||||
try {
|
||||
const result = await api.setup.getCursorStatus();
|
||||
|
||||
if (result.success) {
|
||||
setStatus({
|
||||
installed: result.installed ?? false,
|
||||
version: result.version,
|
||||
path: result.path,
|
||||
auth: result.auth,
|
||||
installCommand: result.installCommand,
|
||||
loginCommand: result.loginCommand,
|
||||
});
|
||||
|
||||
if (result.auth?.authenticated) {
|
||||
toast.success('Cursor CLI is ready!');
|
||||
}
|
||||
} else {
|
||||
toast.error('Failed to check Cursor status');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check Cursor status:', error);
|
||||
toast.error('Failed to check Cursor CLI status');
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkStatus();
|
||||
}, [checkStatus]);
|
||||
|
||||
const handleLogin = async () => {
|
||||
setIsLoggingIn(true);
|
||||
|
||||
try {
|
||||
// Copy login command to clipboard and show instructions
|
||||
if (status?.loginCommand) {
|
||||
await navigator.clipboard.writeText(status.loginCommand);
|
||||
toast.info('Login command copied! Paste in terminal to authenticate.');
|
||||
}
|
||||
|
||||
// Poll for auth status
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60; // 2 minutes with 2s interval
|
||||
|
||||
const pollInterval = setInterval(async () => {
|
||||
attempts++;
|
||||
|
||||
try {
|
||||
const result = await api.setup.getCursorStatus();
|
||||
|
||||
if (result.auth?.authenticated) {
|
||||
clearInterval(pollInterval);
|
||||
setStatus((prev) => (prev ? { ...prev, auth: result.auth } : null));
|
||||
setIsLoggingIn(false);
|
||||
toast.success('Successfully logged in to Cursor!');
|
||||
}
|
||||
} catch {
|
||||
// Ignore polling errors
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
clearInterval(pollInterval);
|
||||
setIsLoggingIn(false);
|
||||
toast.error('Login timed out. Please try again.');
|
||||
}
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
toast.error('Failed to start login process');
|
||||
setIsLoggingIn(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyInstallCommand = async () => {
|
||||
if (status?.installCommand) {
|
||||
await navigator.clipboard.writeText(status.installCommand);
|
||||
toast.success('Install command copied to clipboard!');
|
||||
}
|
||||
};
|
||||
|
||||
const isComplete = status?.installed && status?.auth?.authenticated;
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Terminal className="w-5 h-5" />
|
||||
Cursor CLI Setup
|
||||
<Badge variant="outline" className="ml-2">
|
||||
Optional
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure Cursor CLI as an alternative AI provider. You can skip this and use Claude
|
||||
instead, or configure it later in Settings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{/* Installation Status */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">CLI Installation</span>
|
||||
{isChecking ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
) : status?.installed ? (
|
||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-xs">v{status.version}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
|
||||
<XCircle className="w-4 h-4" />
|
||||
<span className="text-xs">Not installed</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!status?.installed && !isChecking && (
|
||||
<Alert>
|
||||
<AlertDescription className="text-sm space-y-3">
|
||||
<p>Install Cursor CLI to use Cursor models:</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-muted p-2 rounded text-xs font-mono overflow-x-auto">
|
||||
{status?.installCommand || 'curl https://cursor.com/install -fsS | bash'}
|
||||
</code>
|
||||
<Button variant="outline" size="sm" onClick={handleCopyInstallCommand}>
|
||||
Copy
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="p-0 h-auto"
|
||||
onClick={() => window.open('https://cursor.com/docs/cli', '_blank')}
|
||||
>
|
||||
View installation docs
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Authentication Status */}
|
||||
{status?.installed && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Authentication</span>
|
||||
{status.auth?.authenticated ? (
|
||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-xs capitalize">
|
||||
{status.auth.method === 'api_key' ? 'API Key' : 'Browser Login'}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
|
||||
<XCircle className="w-4 h-4" />
|
||||
<span className="text-xs">Not authenticated</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!status.auth?.authenticated && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Run the login command in your terminal, then complete authentication in your
|
||||
browser:
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-muted p-2 rounded text-xs font-mono">
|
||||
{status.loginCommand || 'cursor-agent login'}
|
||||
</code>
|
||||
</div>
|
||||
<Button onClick={handleLogin} disabled={isLoggingIn} className="w-full">
|
||||
{isLoggingIn ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Waiting for login...
|
||||
</>
|
||||
) : (
|
||||
'Copy Command & Wait for Login'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3 pt-4 border-t">
|
||||
<Button variant="outline" onClick={onSkip} className="flex-1">
|
||||
Skip for now
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onComplete}
|
||||
disabled={!isComplete && status?.installed}
|
||||
className="flex-1"
|
||||
>
|
||||
{isComplete ? 'Continue' : 'Complete setup to continue'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={checkStatus}
|
||||
disabled={isChecking}
|
||||
title="Refresh status"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${isChecking ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Info note */}
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
You can always configure Cursor later in Settings → Providers
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CursorSetupStep;
|
||||
```
|
||||
|
||||
### Task 6.2: Update Setup View Steps
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/components/views/setup-view.tsx`
|
||||
|
||||
Add the Cursor step to the wizard:
|
||||
|
||||
```tsx
|
||||
import { CursorSetupStep } from './setup-view/steps/cursor-setup-step';
|
||||
|
||||
// Add to steps configuration
|
||||
const SETUP_STEPS = [
|
||||
// Existing steps...
|
||||
{
|
||||
id: 'claude',
|
||||
title: 'Claude CLI',
|
||||
optional: false,
|
||||
component: ClaudeSetupStep,
|
||||
},
|
||||
// Add Cursor step
|
||||
{
|
||||
id: 'cursor',
|
||||
title: 'Cursor CLI',
|
||||
optional: true,
|
||||
component: CursorSetupStep,
|
||||
},
|
||||
// Remaining steps...
|
||||
{
|
||||
id: 'project',
|
||||
title: 'Project',
|
||||
optional: false,
|
||||
component: ProjectSetupStep,
|
||||
},
|
||||
];
|
||||
|
||||
// In the render function, handle optional steps:
|
||||
function SetupView() {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [skippedSteps, setSkippedSteps] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleSkip = (stepId: string) => {
|
||||
setSkippedSteps((prev) => new Set([...prev, stepId]));
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
setCurrentStep((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const step = SETUP_STEPS[currentStep];
|
||||
const StepComponent = step.component;
|
||||
|
||||
return (
|
||||
<div className="setup-view">
|
||||
{/* Progress indicator */}
|
||||
<div className="flex gap-2 mb-6">
|
||||
{SETUP_STEPS.map((s, i) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className={cn(
|
||||
'flex-1 h-2 rounded',
|
||||
i < currentStep
|
||||
? 'bg-green-500'
|
||||
: i === currentStep
|
||||
? 'bg-blue-500'
|
||||
: skippedSteps.has(s.id)
|
||||
? 'bg-gray-300'
|
||||
: 'bg-gray-200'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Step title */}
|
||||
<h2 className="text-xl font-semibold mb-4">
|
||||
{step.title}
|
||||
{step.optional && <span className="text-sm text-muted-foreground ml-2">(Optional)</span>}
|
||||
</h2>
|
||||
|
||||
{/* Step component */}
|
||||
<StepComponent onComplete={handleComplete} onSkip={() => handleSkip(step.id)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Task 6.3: Add Step Indicator for Optional Steps
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
Add visual indicator for optional vs required steps in the progress bar.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Component Rendering
|
||||
|
||||
1. Start the app with a fresh setup (or clear setup state)
|
||||
2. Navigate through setup steps
|
||||
3. Verify Cursor step appears after Claude step
|
||||
4. Verify "Optional" badge is displayed
|
||||
|
||||
### Test 2: Skip Functionality
|
||||
|
||||
1. Click "Skip for now" on Cursor step
|
||||
2. Verify step is skipped and progress continues
|
||||
3. Verify skipped state is persisted (if applicable)
|
||||
|
||||
### Test 3: Installation Detection
|
||||
|
||||
1. With cursor-agent NOT installed:
|
||||
- Should show "Not installed" status
|
||||
- Should show install command
|
||||
- Continue button should be disabled
|
||||
|
||||
2. With cursor-agent installed but not authenticated:
|
||||
- Should show version number
|
||||
- Should show "Not authenticated" status
|
||||
- Should show login instructions
|
||||
|
||||
3. With cursor-agent installed and authenticated:
|
||||
- Should show green checkmarks
|
||||
- Continue button should be enabled
|
||||
|
||||
### Test 4: Login Flow
|
||||
|
||||
1. Click "Copy Command & Wait for Login"
|
||||
2. Verify command is copied to clipboard
|
||||
3. Run login command in terminal
|
||||
4. Verify status updates after authentication
|
||||
5. Verify success toast appears
|
||||
|
||||
### Test 5: Refresh Status
|
||||
|
||||
1. Click refresh button
|
||||
2. Verify loading state is shown
|
||||
3. Verify status is re-fetched
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [ ] CursorSetupStep component renders correctly
|
||||
- [ ] Step appears in setup wizard flow
|
||||
- [ ] Skip button works and progresses to next step
|
||||
- [ ] Installation status is correctly detected
|
||||
- [ ] Authentication status is correctly detected
|
||||
- [ ] Login command copy works
|
||||
- [ ] Polling for auth status works
|
||||
- [ ] Refresh button updates status
|
||||
- [ ] Error states handled gracefully
|
||||
- [ ] Progress indicator shows optional step differently
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| --------------------------------------------------------------------- | ------ | -------------------- |
|
||||
| `apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx` | Create | Setup step component |
|
||||
| `apps/ui/src/components/views/setup-view.tsx` | Modify | Add step to wizard |
|
||||
|
||||
---
|
||||
|
||||
## Design Notes
|
||||
|
||||
- The step is marked as optional with a badge
|
||||
- Skip button is always available for optional steps
|
||||
- The login flow is asynchronous with polling
|
||||
- Status can be manually refreshed
|
||||
- Error states show clear recovery instructions
|
||||
@@ -1,556 +0,0 @@
|
||||
# Phase 7: Settings View Provider Tabs
|
||||
|
||||
**Status:** `completed`
|
||||
**Dependencies:** Phase 4 (Routes)
|
||||
**Estimated Effort:** Medium (React components)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Create a tabbed interface in Settings for managing different AI providers (Claude and Cursor), with provider-specific configuration options.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 7.1: Create Cursor Settings Tab Component
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx`
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Terminal, CheckCircle2, XCircle, Loader2, RefreshCw, ExternalLink } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/lib/http-api-client';
|
||||
import {
|
||||
CursorModelId,
|
||||
CursorModelConfig,
|
||||
CursorCliConfig,
|
||||
CURSOR_MODEL_MAP,
|
||||
} from '@automaker/types';
|
||||
|
||||
interface CursorStatus {
|
||||
installed: boolean;
|
||||
version?: string;
|
||||
authenticated: boolean;
|
||||
method?: string;
|
||||
}
|
||||
|
||||
export function CursorSettingsTab() {
|
||||
const [status, setStatus] = useState<CursorStatus | null>(null);
|
||||
const [config, setConfig] = useState<CursorCliConfig | null>(null);
|
||||
const [availableModels, setAvailableModels] = useState<CursorModelConfig[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [statusData, configData] = await Promise.all([
|
||||
api.setup.getCursorStatus(),
|
||||
api.setup.getCursorConfig(),
|
||||
]);
|
||||
|
||||
if (statusData.success) {
|
||||
setStatus({
|
||||
installed: statusData.installed ?? false,
|
||||
version: statusData.version,
|
||||
authenticated: statusData.auth?.authenticated ?? false,
|
||||
method: statusData.auth?.method,
|
||||
});
|
||||
}
|
||||
|
||||
if (configData.success) {
|
||||
setConfig(configData.config);
|
||||
setAvailableModels(configData.availableModels || Object.values(CURSOR_MODEL_MAP));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load Cursor settings:', error);
|
||||
toast.error('Failed to load Cursor settings');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const handleDefaultModelChange = async (model: CursorModelId) => {
|
||||
if (!config) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const result = await api.setup.setCursorDefaultModel(model);
|
||||
|
||||
if (result.success) {
|
||||
setConfig({ ...config, defaultModel: model });
|
||||
toast.success('Default model updated');
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to update default model');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update default model');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelToggle = async (model: CursorModelId, enabled: boolean) => {
|
||||
if (!config) return;
|
||||
|
||||
const newModels = enabled
|
||||
? [...(config.models || []), model]
|
||||
: (config.models || []).filter((m) => m !== model);
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const result = await api.setup.setCursorModels(newModels);
|
||||
|
||||
if (result.success) {
|
||||
setConfig({ ...config, models: newModels });
|
||||
} else {
|
||||
toast.error(result.error || 'Failed to update models');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update models');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Status Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Terminal className="w-5 h-5" />
|
||||
Cursor CLI Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Installation */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Installation</span>
|
||||
{status?.installed ? (
|
||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-xs font-mono">v{status.version}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-destructive">
|
||||
<XCircle className="w-4 h-4" />
|
||||
<span className="text-xs">Not installed</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Authentication */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Authentication</span>
|
||||
{status?.authenticated ? (
|
||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-xs capitalize">
|
||||
{status.method === 'api_key' ? 'API Key' : 'Browser Login'}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
|
||||
<XCircle className="w-4 h-4" />
|
||||
<span className="text-xs">Not authenticated</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button variant="outline" size="sm" onClick={loadData}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh Status
|
||||
</Button>
|
||||
{!status?.installed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => window.open('https://cursor.com/docs/cli', '_blank')}
|
||||
>
|
||||
Installation Guide
|
||||
<ExternalLink className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Model Configuration */}
|
||||
{status?.installed && config && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Model Configuration</CardTitle>
|
||||
<CardDescription>
|
||||
Configure which Cursor models are available and set the default
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Default Model */}
|
||||
<div className="space-y-2">
|
||||
<Label>Default Model</Label>
|
||||
<Select
|
||||
value={config.defaultModel || 'auto'}
|
||||
onValueChange={(v) => handleDefaultModelChange(v as CursorModelId)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(config.models || ['auto']).map((modelId) => {
|
||||
const model = CURSOR_MODEL_MAP[modelId];
|
||||
if (!model) return null;
|
||||
return (
|
||||
<SelectItem key={modelId} value={modelId}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{model.label}</span>
|
||||
{model.hasThinking && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Thinking
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Enabled Models */}
|
||||
<div className="space-y-3">
|
||||
<Label>Available Models</Label>
|
||||
<div className="grid gap-3">
|
||||
{availableModels.map((model) => {
|
||||
const isEnabled = config.models?.includes(model.id) ?? false;
|
||||
const isAuto = model.id === 'auto';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
className="flex items-center justify-between p-3 rounded-lg border bg-card"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={isEnabled}
|
||||
onCheckedChange={(checked) => handleModelToggle(model.id, !!checked)}
|
||||
disabled={isSaving || isAuto}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{model.label}</span>
|
||||
{model.hasThinking && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Thinking
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{model.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={model.tier === 'free' ? 'default' : 'secondary'}>
|
||||
{model.tier}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Not Installed State */}
|
||||
{!status?.installed && (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-muted-foreground">
|
||||
<Terminal className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>Cursor CLI is not installed.</p>
|
||||
<p className="text-sm mt-2">Install it to use Cursor models in AutoMaker.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CursorSettingsTab;
|
||||
```
|
||||
|
||||
### Task 7.2: Create Provider Tabs Container
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx`
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Bot, Terminal } from 'lucide-react';
|
||||
import { CursorSettingsTab } from './cursor-settings-tab';
|
||||
import { ClaudeSettingsTab } from './claude-settings-tab';
|
||||
|
||||
interface ProviderTabsProps {
|
||||
defaultTab?: 'claude' | 'cursor';
|
||||
}
|
||||
|
||||
export function ProviderTabs({ defaultTab = 'claude' }: ProviderTabsProps) {
|
||||
return (
|
||||
<Tabs defaultValue={defaultTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="claude" className="flex items-center gap-2">
|
||||
<Bot className="w-4 h-4" />
|
||||
Claude
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cursor" className="flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4" />
|
||||
Cursor
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="claude">
|
||||
<ClaudeSettingsTab />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="cursor">
|
||||
<CursorSettingsTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProviderTabs;
|
||||
```
|
||||
|
||||
### Task 7.3: Create Claude Settings Tab (if not exists)
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx`
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Bot, CheckCircle2, XCircle, Loader2, RefreshCw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/lib/http-api-client';
|
||||
|
||||
interface ClaudeStatus {
|
||||
installed: boolean;
|
||||
version?: string;
|
||||
authenticated: boolean;
|
||||
method?: string;
|
||||
}
|
||||
|
||||
export function ClaudeSettingsTab() {
|
||||
const [status, setStatus] = useState<ClaudeStatus | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const loadStatus = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await api.setup.getClaudeStatus();
|
||||
|
||||
if (result.success) {
|
||||
setStatus({
|
||||
installed: result.installed ?? true,
|
||||
version: result.version,
|
||||
authenticated: result.authenticated ?? false,
|
||||
method: result.method,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load Claude status:', error);
|
||||
toast.error('Failed to load Claude status');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadStatus();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Bot className="w-5 h-5" />
|
||||
Claude Status
|
||||
</CardTitle>
|
||||
<CardDescription>Claude is the primary AI provider for AutoMaker</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">SDK Status</span>
|
||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-xs">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm">Authentication</span>
|
||||
{status?.authenticated ? (
|
||||
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span className="text-xs capitalize">{status.method}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
|
||||
<XCircle className="w-4 h-4" />
|
||||
<span className="text-xs">Not authenticated</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={loadStatus}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Refresh Status
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaudeSettingsTab;
|
||||
```
|
||||
|
||||
### Task 7.4: Update Settings View Navigation
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/components/views/settings-view/config/navigation.ts`
|
||||
|
||||
Add or update providers section:
|
||||
|
||||
```typescript
|
||||
export const SETTINGS_NAVIGATION = [
|
||||
// Existing sections...
|
||||
{
|
||||
id: 'providers',
|
||||
label: 'AI Providers',
|
||||
icon: 'bot',
|
||||
description: 'Configure Claude and Cursor AI providers',
|
||||
},
|
||||
// ... other sections
|
||||
];
|
||||
```
|
||||
|
||||
### Task 7.5: Integrate Provider Tabs in Settings
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
Update the settings view to render ProviderTabs for the providers section.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Tab Switching
|
||||
|
||||
1. Navigate to Settings → Providers
|
||||
2. Click on "Claude" tab
|
||||
3. Verify Claude settings are displayed
|
||||
4. Click on "Cursor" tab
|
||||
5. Verify Cursor settings are displayed
|
||||
|
||||
### Test 2: Cursor Status Display
|
||||
|
||||
1. With Cursor CLI installed: verify version is shown
|
||||
2. With Cursor authenticated: verify green checkmark
|
||||
3. Without Cursor installed: verify "Not installed" state
|
||||
|
||||
### Test 3: Model Selection
|
||||
|
||||
1. Enable/disable models via checkboxes
|
||||
2. Verify changes persist after refresh
|
||||
3. Change default model
|
||||
4. Verify default is highlighted in selector
|
||||
|
||||
### Test 4: Responsive Design
|
||||
|
||||
1. Test on different screen sizes
|
||||
2. Verify tabs are usable on mobile
|
||||
3. Verify model list scrolls properly
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [ ] ProviderTabs component renders correctly
|
||||
- [ ] Tab switching works smoothly
|
||||
- [ ] CursorSettingsTab shows correct status
|
||||
- [ ] ClaudeSettingsTab shows correct status
|
||||
- [ ] Model checkboxes toggle state
|
||||
- [ ] Default model selector works
|
||||
- [ ] Settings persist after page refresh
|
||||
- [ ] Loading states displayed
|
||||
- [ ] Error states handled gracefully
|
||||
- [ ] Settings navigation includes providers
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| ------------------------------------------------------------------------------ | ------ | ------------- |
|
||||
| `apps/ui/src/components/views/settings-view/providers/cursor-settings-tab.tsx` | Create | Cursor config |
|
||||
| `apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx` | Create | Claude config |
|
||||
| `apps/ui/src/components/views/settings-view/providers/provider-tabs.tsx` | Create | Tab container |
|
||||
| `apps/ui/src/components/views/settings-view/config/navigation.ts` | Modify | Add section |
|
||||
|
||||
---
|
||||
|
||||
## Design Notes
|
||||
|
||||
- Tabs use consistent icons (Bot for Claude, Terminal for Cursor)
|
||||
- Model cards show tier badges (free/pro)
|
||||
- Thinking models have a "Thinking" badge
|
||||
- The "auto" model cannot be disabled
|
||||
- Settings auto-save on change (no explicit save button)
|
||||
@@ -1,590 +0,0 @@
|
||||
# Phase 8: AI Profiles Integration
|
||||
|
||||
**Status:** `completed`
|
||||
**Dependencies:** Phase 1 (Types), Phase 7 (Settings)
|
||||
**Estimated Effort:** Medium (UI + types)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Extend the AI Profiles system to support Cursor as a provider, with proper handling of Cursor's embedded thinking mode (via model ID) vs Claude's separate thinking level.
|
||||
|
||||
---
|
||||
|
||||
## Key Concept: Thinking Mode Handling
|
||||
|
||||
### Claude Approach
|
||||
|
||||
- Separate `thinkingLevel` property: `'none' | 'low' | 'medium' | 'high' | 'ultrathink'`
|
||||
- Applied to any Claude model
|
||||
|
||||
### Cursor Approach
|
||||
|
||||
- Thinking is **embedded in the model ID**
|
||||
- Examples: `claude-sonnet-4` (no thinking) vs `claude-sonnet-4-thinking` (with thinking)
|
||||
- No separate thinking level selector needed for Cursor profiles
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 8.1: Update AIProfile Type
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `libs/types/src/settings.ts`
|
||||
|
||||
Update the AIProfile interface:
|
||||
|
||||
```typescript
|
||||
import { CursorModelId } from './cursor-models';
|
||||
|
||||
/**
|
||||
* AI Profile - saved configuration for different use cases
|
||||
*/
|
||||
export interface AIProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isBuiltIn: boolean;
|
||||
icon?: string;
|
||||
|
||||
// Provider selection
|
||||
provider: ModelProvider; // 'claude' | 'cursor'
|
||||
|
||||
// Claude-specific
|
||||
model?: AgentModel; // 'opus' | 'sonnet' | 'haiku'
|
||||
thinkingLevel?: ThinkingLevel; // 'none' | 'low' | 'medium' | 'high' | 'ultrathink'
|
||||
|
||||
// Cursor-specific
|
||||
cursorModel?: CursorModelId; // 'auto' | 'claude-sonnet-4' | 'gpt-4o' | etc.
|
||||
// Note: For Cursor, thinking is in the model ID (e.g., 'claude-sonnet-4-thinking')
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to determine if a profile uses thinking mode
|
||||
*/
|
||||
export function profileHasThinking(profile: AIProfile): boolean {
|
||||
if (profile.provider === 'claude') {
|
||||
return profile.thinkingLevel !== undefined && profile.thinkingLevel !== 'none';
|
||||
}
|
||||
|
||||
if (profile.provider === 'cursor') {
|
||||
const model = profile.cursorModel || 'auto';
|
||||
return model.includes('thinking') || model === 'o3-mini';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective model string for execution
|
||||
*/
|
||||
export function getProfileModelString(profile: AIProfile): string {
|
||||
if (profile.provider === 'cursor') {
|
||||
return `cursor-${profile.cursorModel || 'auto'}`;
|
||||
}
|
||||
|
||||
// Claude
|
||||
return profile.model || 'sonnet';
|
||||
}
|
||||
```
|
||||
|
||||
### Task 8.2: Update Profile Form Component
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/components/views/profiles-view/components/profile-form.tsx`
|
||||
|
||||
Add Cursor-specific fields:
|
||||
|
||||
```tsx
|
||||
import React, { useState } from 'react';
|
||||
import { Bot, Terminal } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
AIProfile,
|
||||
AgentModel,
|
||||
ModelProvider,
|
||||
ThinkingLevel,
|
||||
CursorModelId,
|
||||
CURSOR_MODEL_MAP,
|
||||
cursorModelHasThinking,
|
||||
} from '@automaker/types';
|
||||
|
||||
interface ProfileFormProps {
|
||||
profile: AIProfile;
|
||||
onSave: (profile: AIProfile) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ProfileForm({ profile, onSave, onCancel }: ProfileFormProps) {
|
||||
const [formData, setFormData] = useState<AIProfile>(profile);
|
||||
|
||||
const handleProviderChange = (provider: ModelProvider) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
provider,
|
||||
// Reset provider-specific fields
|
||||
model: provider === 'claude' ? 'sonnet' : undefined,
|
||||
thinkingLevel: provider === 'claude' ? 'none' : undefined,
|
||||
cursorModel: provider === 'cursor' ? 'auto' : undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Name & Description */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Profile Name</Label>
|
||||
<Input
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData((p) => ({ ...p, name: e.target.value }))}
|
||||
placeholder="My Profile"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<Textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData((p) => ({ ...p, description: e.target.value }))}
|
||||
placeholder="Describe when to use this profile..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Provider Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>AI Provider</Label>
|
||||
<Select value={formData.provider} onValueChange={handleProviderChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="claude">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="w-4 h-4" />
|
||||
Claude (Anthropic)
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="cursor">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4" />
|
||||
Cursor CLI
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Claude-specific settings */}
|
||||
{formData.provider === 'claude' && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label>Model</Label>
|
||||
<Select
|
||||
value={formData.model || 'sonnet'}
|
||||
onValueChange={(v) => setFormData((p) => ({ ...p, model: v as AgentModel }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="haiku">Haiku (Fast)</SelectItem>
|
||||
<SelectItem value="sonnet">Sonnet (Balanced)</SelectItem>
|
||||
<SelectItem value="opus">Opus (Powerful)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Thinking Level</Label>
|
||||
<Select
|
||||
value={formData.thinkingLevel || 'none'}
|
||||
onValueChange={(v) =>
|
||||
setFormData((p) => ({ ...p, thinkingLevel: v as ThinkingLevel }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="ultrathink">Ultra</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cursor-specific settings */}
|
||||
{formData.provider === 'cursor' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Cursor Model</Label>
|
||||
<Select
|
||||
value={formData.cursorModel || 'auto'}
|
||||
onValueChange={(v) => setFormData((p) => ({ ...p, cursorModel: v as CursorModelId }))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(CURSOR_MODEL_MAP).map(([id, config]) => (
|
||||
<SelectItem key={id} value={id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{config.label}</span>
|
||||
{config.hasThinking && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Thinking
|
||||
</Badge>
|
||||
)}
|
||||
<Badge
|
||||
variant={config.tier === 'free' ? 'default' : 'secondary'}
|
||||
className="text-xs"
|
||||
>
|
||||
{config.tier}
|
||||
</Badge>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Info about thinking models */}
|
||||
{formData.cursorModel && cursorModelHasThinking(formData.cursorModel) && (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
This model has built-in extended thinking capabilities.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Save Profile</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Task 8.3: Update Profile Card Display
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/components/views/profiles-view/components/profile-card.tsx`
|
||||
|
||||
Show provider-specific info:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { Bot, Terminal } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { AIProfile, CURSOR_MODEL_MAP, profileHasThinking } from '@automaker/types';
|
||||
|
||||
interface ProfileCardProps {
|
||||
profile: AIProfile;
|
||||
onEdit: (profile: AIProfile) => void;
|
||||
onDelete: (profile: AIProfile) => void;
|
||||
}
|
||||
|
||||
export function ProfileCard({ profile, onEdit, onDelete }: ProfileCardProps) {
|
||||
const hasThinking = profileHasThinking(profile);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
{profile.provider === 'cursor' ? (
|
||||
<Terminal className="w-4 h-4" />
|
||||
) : (
|
||||
<Bot className="w-4 h-4" />
|
||||
)}
|
||||
{profile.name}
|
||||
</CardTitle>
|
||||
{profile.isBuiltIn && <Badge variant="secondary">Built-in</Badge>}
|
||||
</div>
|
||||
<CardDescription>{profile.description}</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{/* Provider badge */}
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{profile.provider}
|
||||
</Badge>
|
||||
|
||||
{/* Model badge */}
|
||||
<Badge variant="outline">
|
||||
{profile.provider === 'cursor'
|
||||
? CURSOR_MODEL_MAP[profile.cursorModel || 'auto']?.label || profile.cursorModel
|
||||
: profile.model}
|
||||
</Badge>
|
||||
|
||||
{/* Thinking badge */}
|
||||
{hasThinking && <Badge variant="default">Thinking</Badge>}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{!profile.isBuiltIn && (
|
||||
<CardFooter className="pt-0">
|
||||
<div className="flex gap-2 ml-auto">
|
||||
<Button variant="ghost" size="sm" onClick={() => onEdit(profile)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => onDelete(profile)}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Task 8.4: Add Default Cursor Profiles
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
**File:** `apps/ui/src/components/views/profiles-view/constants.ts`
|
||||
|
||||
Add built-in Cursor profiles:
|
||||
|
||||
```typescript
|
||||
import { AIProfile } from '@automaker/types';
|
||||
|
||||
export const DEFAULT_PROFILES: AIProfile[] = [
|
||||
// Existing Claude profiles...
|
||||
{
|
||||
id: 'claude-default',
|
||||
name: 'Claude Default',
|
||||
description: 'Balanced Claude Sonnet model',
|
||||
provider: 'claude',
|
||||
model: 'sonnet',
|
||||
thinkingLevel: 'none',
|
||||
isBuiltIn: true,
|
||||
icon: 'bot',
|
||||
},
|
||||
// ... other Claude profiles
|
||||
|
||||
// Cursor profiles
|
||||
{
|
||||
id: 'cursor-auto',
|
||||
name: 'Cursor Auto',
|
||||
description: 'Let Cursor choose the best model automatically',
|
||||
provider: 'cursor',
|
||||
cursorModel: 'auto',
|
||||
isBuiltIn: true,
|
||||
icon: 'terminal',
|
||||
},
|
||||
{
|
||||
id: 'cursor-fast',
|
||||
name: 'Cursor Fast',
|
||||
description: 'Quick responses with GPT-4o Mini',
|
||||
provider: 'cursor',
|
||||
cursorModel: 'gpt-4o-mini',
|
||||
isBuiltIn: true,
|
||||
icon: 'zap',
|
||||
},
|
||||
{
|
||||
id: 'cursor-thinking',
|
||||
name: 'Cursor Thinking',
|
||||
description: 'Claude Sonnet 4 with extended thinking for complex tasks',
|
||||
provider: 'cursor',
|
||||
cursorModel: 'claude-sonnet-4-thinking',
|
||||
isBuiltIn: true,
|
||||
icon: 'brain',
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### Task 8.5: Update Profile Validation
|
||||
|
||||
**Status:** `completed`
|
||||
|
||||
Add validation for profile data:
|
||||
|
||||
```typescript
|
||||
import { AIProfile, CURSOR_MODEL_MAP } from '@automaker/types';
|
||||
|
||||
export function validateProfile(profile: AIProfile): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!profile.name?.trim()) {
|
||||
errors.push('Profile name is required');
|
||||
}
|
||||
|
||||
if (!['claude', 'cursor'].includes(profile.provider)) {
|
||||
errors.push('Invalid provider');
|
||||
}
|
||||
|
||||
if (profile.provider === 'claude') {
|
||||
if (!profile.model) {
|
||||
errors.push('Claude model is required');
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.provider === 'cursor') {
|
||||
if (profile.cursorModel && !(profile.cursorModel in CURSOR_MODEL_MAP)) {
|
||||
errors.push('Invalid Cursor model');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Profile Creation with Cursor
|
||||
|
||||
1. Navigate to Profiles view
|
||||
2. Click "Create Profile"
|
||||
3. Select "Cursor CLI" as provider
|
||||
4. Select a Cursor model
|
||||
5. Save the profile
|
||||
6. Verify it appears in the list with correct badges
|
||||
|
||||
### Test 2: Thinking Mode Detection
|
||||
|
||||
```typescript
|
||||
import { profileHasThinking } from '@automaker/types';
|
||||
|
||||
// Claude with thinking
|
||||
const claudeThinking: AIProfile = {
|
||||
id: '1',
|
||||
name: 'Test',
|
||||
description: '',
|
||||
provider: 'claude',
|
||||
model: 'sonnet',
|
||||
thinkingLevel: 'high',
|
||||
isBuiltIn: false,
|
||||
};
|
||||
console.assert(profileHasThinking(claudeThinking) === true);
|
||||
|
||||
// Claude without thinking
|
||||
const claudeNoThinking: AIProfile = {
|
||||
id: '2',
|
||||
name: 'Test',
|
||||
description: '',
|
||||
provider: 'claude',
|
||||
model: 'sonnet',
|
||||
thinkingLevel: 'none',
|
||||
isBuiltIn: false,
|
||||
};
|
||||
console.assert(profileHasThinking(claudeNoThinking) === false);
|
||||
|
||||
// Cursor with thinking model
|
||||
const cursorThinking: AIProfile = {
|
||||
id: '3',
|
||||
name: 'Test',
|
||||
description: '',
|
||||
provider: 'cursor',
|
||||
cursorModel: 'claude-sonnet-4-thinking',
|
||||
isBuiltIn: false,
|
||||
};
|
||||
console.assert(profileHasThinking(cursorThinking) === true);
|
||||
|
||||
// Cursor without thinking
|
||||
const cursorNoThinking: AIProfile = {
|
||||
id: '4',
|
||||
name: 'Test',
|
||||
description: '',
|
||||
provider: 'cursor',
|
||||
cursorModel: 'gpt-4o',
|
||||
isBuiltIn: false,
|
||||
};
|
||||
console.assert(profileHasThinking(cursorNoThinking) === false);
|
||||
|
||||
console.log('All thinking detection tests passed!');
|
||||
```
|
||||
|
||||
### Test 3: Provider Switching
|
||||
|
||||
1. Create a new profile
|
||||
2. Select Claude as provider
|
||||
3. Configure Claude options
|
||||
4. Switch to Cursor
|
||||
5. Verify Claude options are hidden
|
||||
6. Verify Cursor options are shown
|
||||
7. Previous selections should be cleared
|
||||
|
||||
### Test 4: Built-in Profiles
|
||||
|
||||
1. Navigate to Profiles view
|
||||
2. Verify Cursor built-in profiles appear
|
||||
3. Verify they cannot be edited/deleted
|
||||
4. Verify they show correct badges
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [ ] AIProfile type extended with Cursor fields
|
||||
- [ ] `profileHasThinking()` works for both providers
|
||||
- [ ] Profile form shows provider selector
|
||||
- [ ] Claude options shown only for Claude provider
|
||||
- [ ] Cursor options shown only for Cursor provider
|
||||
- [ ] Cursor models show thinking badge where applicable
|
||||
- [ ] Built-in Cursor profiles added
|
||||
- [ ] Profile cards display provider info
|
||||
- [ ] Profile validation works
|
||||
- [ ] Profiles persist correctly
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| ------------------------------------------------------------------------ | ------ | ------------------------------ |
|
||||
| `libs/types/src/settings.ts` | Modify | Add Cursor fields to AIProfile |
|
||||
| `apps/ui/src/components/views/profiles-view/components/profile-form.tsx` | Modify | Add Cursor UI |
|
||||
| `apps/ui/src/components/views/profiles-view/components/profile-card.tsx` | Modify | Show provider info |
|
||||
| `apps/ui/src/components/views/profiles-view/constants.ts` | Modify | Add Cursor profiles |
|
||||
|
||||
---
|
||||
|
||||
## Design Notes
|
||||
|
||||
- Provider selection is the first choice in profile form
|
||||
- Switching providers resets model-specific options
|
||||
- Cursor thinking is determined by model ID, not separate field
|
||||
- Built-in profiles provide good starting points
|
||||
- Profile cards show provider icon and model badges
|
||||
@@ -1,451 +0,0 @@
|
||||
# Phase 9: Task Execution Integration
|
||||
|
||||
**Status:** `completed`
|
||||
**Dependencies:** Phase 3 (Factory), Phase 8 (Profiles)
|
||||
**Estimated Effort:** Medium (service updates)
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Update the task execution flow (agent-service, auto-mode-service) to use the ProviderFactory for model routing, ensuring Cursor models are executed via CursorProvider.
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 9.1: Update Agent Service
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
**File:** `apps/server/src/services/agent-service.ts`
|
||||
|
||||
Update to use ProviderFactory:
|
||||
|
||||
```typescript
|
||||
import { ProviderFactory } from '../providers/provider-factory';
|
||||
import { getProfileModelString, profileHasThinking } from '@automaker/types';
|
||||
|
||||
export class AgentService {
|
||||
// ...existing code...
|
||||
|
||||
/**
|
||||
* Execute a chat message using the appropriate provider
|
||||
*/
|
||||
async executeChat(sessionId: string, message: string, options: ChatOptions = {}): Promise<void> {
|
||||
const session = this.getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Session ${sessionId} not found`);
|
||||
}
|
||||
|
||||
// Determine effective model
|
||||
const profile = options.profile;
|
||||
let effectiveModel: string;
|
||||
|
||||
if (profile) {
|
||||
effectiveModel = getProfileModelString(profile);
|
||||
} else {
|
||||
effectiveModel = options.model || session.model || 'sonnet';
|
||||
}
|
||||
|
||||
// Get provider for this model
|
||||
const provider = ProviderFactory.getProviderForModel(effectiveModel, {
|
||||
cwd: session.workDir,
|
||||
});
|
||||
|
||||
const providerName = provider.getName();
|
||||
this.logger.debug(`[AgentService] Using ${providerName} provider for model ${effectiveModel}`);
|
||||
|
||||
// Build execution options
|
||||
const executeOptions: ExecuteOptions = {
|
||||
prompt: message,
|
||||
model: effectiveModel,
|
||||
cwd: session.workDir,
|
||||
systemPrompt: this.buildSystemPrompt(session, options),
|
||||
maxTurns: options.maxTurns || 100,
|
||||
allowedTools: options.allowedTools || TOOL_PRESETS.chat,
|
||||
abortController: session.abortController,
|
||||
conversationHistory: session.conversationHistory,
|
||||
sdkSessionId: session.sdkSessionId,
|
||||
};
|
||||
|
||||
// Add thinking level for Claude
|
||||
if (providerName === 'claude' && profile?.thinkingLevel) {
|
||||
executeOptions.thinkingLevel = profile.thinkingLevel;
|
||||
}
|
||||
|
||||
try {
|
||||
// Stream from provider
|
||||
const stream = provider.executeQuery(executeOptions);
|
||||
|
||||
for await (const msg of stream) {
|
||||
// Capture session ID
|
||||
if (msg.session_id && !session.sdkSessionId) {
|
||||
session.sdkSessionId = msg.session_id;
|
||||
}
|
||||
|
||||
// Process message and emit events
|
||||
this.processProviderMessage(sessionId, msg);
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleProviderError(sessionId, error, providerName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a provider message and emit appropriate events
|
||||
*/
|
||||
private processProviderMessage(sessionId: string, msg: ProviderMessage): void {
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text' && block.text) {
|
||||
this.emitAgentEvent(sessionId, {
|
||||
type: 'stream',
|
||||
content: block.text,
|
||||
});
|
||||
} else if (block.type === 'tool_use') {
|
||||
this.emitAgentEvent(sessionId, {
|
||||
type: 'tool_use',
|
||||
tool: {
|
||||
name: block.name,
|
||||
input: block.input,
|
||||
id: block.tool_use_id,
|
||||
},
|
||||
});
|
||||
} else if (block.type === 'tool_result') {
|
||||
this.emitAgentEvent(sessionId, {
|
||||
type: 'tool_result',
|
||||
toolId: block.tool_use_id,
|
||||
content: block.content,
|
||||
});
|
||||
} else if (block.type === 'thinking' && block.thinking) {
|
||||
this.emitAgentEvent(sessionId, {
|
||||
type: 'thinking',
|
||||
content: block.thinking,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (msg.type === 'result') {
|
||||
this.emitAgentEvent(sessionId, {
|
||||
type: 'complete',
|
||||
content: msg.result || '',
|
||||
});
|
||||
} else if (msg.type === 'error') {
|
||||
this.emitAgentEvent(sessionId, {
|
||||
type: 'error',
|
||||
error: msg.error || 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle provider-specific errors
|
||||
*/
|
||||
private handleProviderError(sessionId: string, error: any, providerName: string): void {
|
||||
let errorMessage = error.message || 'Unknown error';
|
||||
let suggestion = error.suggestion;
|
||||
|
||||
// Add provider context
|
||||
if (providerName === 'cursor' && error.code) {
|
||||
switch (error.code) {
|
||||
case 'CURSOR_NOT_AUTHENTICATED':
|
||||
suggestion = 'Run "cursor-agent login" in your terminal';
|
||||
break;
|
||||
case 'CURSOR_RATE_LIMITED':
|
||||
suggestion = 'Wait a few minutes or upgrade to Cursor Pro';
|
||||
break;
|
||||
case 'CURSOR_NOT_INSTALLED':
|
||||
suggestion = 'Install Cursor CLI: curl https://cursor.com/install -fsS | bash';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.emitAgentEvent(sessionId, {
|
||||
type: 'error',
|
||||
error: errorMessage,
|
||||
suggestion,
|
||||
provider: providerName,
|
||||
});
|
||||
|
||||
this.logger.error(`[AgentService] ${providerName} error:`, error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task 9.2: Update Auto Mode Service
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
**File:** `apps/server/src/services/auto-mode-service.ts`
|
||||
|
||||
Update the `runAgent` method:
|
||||
|
||||
```typescript
|
||||
import { ProviderFactory } from '../providers/provider-factory';
|
||||
import { getProfileModelString } from '@automaker/types';
|
||||
|
||||
export class AutoModeService {
|
||||
// ...existing code...
|
||||
|
||||
/**
|
||||
* Run the agent for a task
|
||||
*/
|
||||
private async runAgent(task: Task, options: AutoModeOptions): Promise<AgentResult> {
|
||||
const { workDir, profile, maxTurns } = options;
|
||||
|
||||
// Determine model from profile or task
|
||||
let model: string;
|
||||
if (profile) {
|
||||
model = getProfileModelString(profile);
|
||||
} else {
|
||||
model = task.model || 'sonnet';
|
||||
}
|
||||
|
||||
// Get provider
|
||||
const provider = ProviderFactory.getProviderForModel(model, { cwd: workDir });
|
||||
const providerName = provider.getName();
|
||||
|
||||
this.logger.info(`[AutoMode] Running with ${providerName} provider, model: ${model}`);
|
||||
|
||||
// Build execution options
|
||||
const executeOptions: ExecuteOptions = {
|
||||
prompt: this.buildPrompt(task),
|
||||
model,
|
||||
cwd: workDir,
|
||||
systemPrompt: options.systemPrompt,
|
||||
maxTurns: maxTurns || MAX_TURNS.extended,
|
||||
allowedTools: options.allowedTools || TOOL_PRESETS.fullAccess,
|
||||
abortController: options.abortController,
|
||||
};
|
||||
|
||||
let responseText = '';
|
||||
const toolCalls: ToolCall[] = [];
|
||||
|
||||
try {
|
||||
const stream = provider.executeQuery(executeOptions);
|
||||
|
||||
for await (const msg of stream) {
|
||||
// Emit progress events
|
||||
this.emitProgress(task.id, msg, providerName);
|
||||
|
||||
// Collect response
|
||||
if (msg.type === 'assistant' && msg.message?.content) {
|
||||
for (const block of msg.message.content) {
|
||||
if (block.type === 'text') {
|
||||
responseText += block.text || '';
|
||||
} else if (block.type === 'tool_use') {
|
||||
toolCalls.push({
|
||||
id: block.tool_use_id,
|
||||
name: block.name,
|
||||
input: block.input,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
response: responseText,
|
||||
toolCalls,
|
||||
provider: providerName,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
suggestion: error.suggestion,
|
||||
provider: providerName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit progress event for UI updates
|
||||
*/
|
||||
private emitProgress(taskId: string, msg: ProviderMessage, provider: string): void {
|
||||
// Emit event for log viewer and progress tracking
|
||||
this.events.emit('auto-mode:event', {
|
||||
taskId,
|
||||
provider,
|
||||
message: msg,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task 9.3: Update Model Selector in Board View
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
**File:** `apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx`
|
||||
|
||||
Add Cursor models to selection:
|
||||
|
||||
```tsx
|
||||
import { CURSOR_MODEL_MAP, CursorModelId } from '@automaker/types';
|
||||
|
||||
interface ModelOption {
|
||||
id: string;
|
||||
label: string;
|
||||
provider: 'claude' | 'cursor';
|
||||
hasThinking?: boolean;
|
||||
}
|
||||
|
||||
const MODEL_OPTIONS: ModelOption[] = [
|
||||
// Claude models
|
||||
{ id: 'haiku', label: 'Claude Haiku', provider: 'claude' },
|
||||
{ id: 'sonnet', label: 'Claude Sonnet', provider: 'claude' },
|
||||
{ id: 'opus', label: 'Claude Opus', provider: 'claude' },
|
||||
|
||||
// Cursor models
|
||||
...Object.entries(CURSOR_MODEL_MAP).map(([id, config]) => ({
|
||||
id: `cursor-${id}`,
|
||||
label: `Cursor: ${config.label}`,
|
||||
provider: 'cursor' as const,
|
||||
hasThinking: config.hasThinking,
|
||||
})),
|
||||
];
|
||||
|
||||
// In the dialog form:
|
||||
<div className="space-y-2">
|
||||
<Label>Model</Label>
|
||||
<Select value={selectedModel} onValueChange={setSelectedModel}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Claude</SelectLabel>
|
||||
{MODEL_OPTIONS.filter((m) => m.provider === 'claude').map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Cursor</SelectLabel>
|
||||
{MODEL_OPTIONS.filter((m) => m.provider === 'cursor').map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{model.label}
|
||||
{model.hasThinking && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Thinking
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>;
|
||||
```
|
||||
|
||||
### Task 9.4: Update Feature Execution with Provider Tracking
|
||||
|
||||
**Status:** `pending`
|
||||
|
||||
Track which provider executed each feature for UI display:
|
||||
|
||||
```typescript
|
||||
interface FeatureExecution {
|
||||
id: string;
|
||||
featureId: string;
|
||||
model: string;
|
||||
provider: 'claude' | 'cursor';
|
||||
startTime: number;
|
||||
endTime?: number;
|
||||
status: 'running' | 'completed' | 'failed';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Store provider info in execution results
|
||||
const execution: FeatureExecution = {
|
||||
id: generateId(),
|
||||
featureId: feature.id,
|
||||
model: effectiveModel,
|
||||
provider: ProviderFactory.getProviderNameForModel(effectiveModel),
|
||||
startTime: Date.now(),
|
||||
status: 'running',
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Test 1: Claude Model Execution
|
||||
|
||||
1. Create a task with a Claude model (e.g., `sonnet`)
|
||||
2. Execute the task
|
||||
3. Verify ClaudeProvider is used
|
||||
4. Verify output streams correctly
|
||||
5. Verify tool calls work
|
||||
|
||||
### Test 2: Cursor Model Execution
|
||||
|
||||
1. Create a task with a Cursor model (e.g., `cursor-auto`)
|
||||
2. Execute the task
|
||||
3. Verify CursorProvider is used
|
||||
4. Verify output streams correctly
|
||||
5. Verify tool calls work
|
||||
|
||||
### Test 3: Profile-Based Execution
|
||||
|
||||
1. Create a Cursor profile
|
||||
2. Use that profile for a task
|
||||
3. Verify correct provider is selected
|
||||
4. Verify profile settings are applied
|
||||
|
||||
### Test 4: Error Handling
|
||||
|
||||
1. Use Cursor model without CLI installed
|
||||
2. Verify appropriate error message
|
||||
3. Verify suggestion is shown
|
||||
4. Verify execution can be retried
|
||||
|
||||
### Test 5: Mixed Provider Session
|
||||
|
||||
1. Run a task with Claude
|
||||
2. Run another task with Cursor
|
||||
3. Verify both execute correctly
|
||||
4. Verify logs show correct provider info
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before marking this phase complete:
|
||||
|
||||
- [ ] AgentService uses ProviderFactory
|
||||
- [ ] AutoModeService uses ProviderFactory
|
||||
- [ ] Claude models route to ClaudeProvider
|
||||
- [ ] Cursor models route to CursorProvider
|
||||
- [ ] Profile model string conversion works
|
||||
- [ ] Provider errors include suggestions
|
||||
- [ ] Progress events include provider info
|
||||
- [ ] Model selector includes Cursor models
|
||||
- [ ] Execution results track provider
|
||||
- [ ] Log viewer shows provider context
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Action | Description |
|
||||
| ------------------------------------------------------------------------ | ------ | ------------------- |
|
||||
| `apps/server/src/services/agent-service.ts` | Modify | Use ProviderFactory |
|
||||
| `apps/server/src/services/auto-mode-service.ts` | Modify | Use ProviderFactory |
|
||||
| `apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx` | Modify | Add Cursor models |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Provider selection happens at execution time, not configuration time
|
||||
- Session state may span provider switches
|
||||
- Error handling is provider-aware
|
||||
- Progress events include provider for UI grouping
|
||||
@@ -1,787 +0,0 @@
|
||||
# Per-Phase AI Provider Configuration - Implementation Plan
|
||||
|
||||
> **Created**: 2024-12-30
|
||||
> **Approach**: UI-First with incremental wiring
|
||||
> **Estimated Total Effort**: 20-25 hours
|
||||
|
||||
## Overview
|
||||
|
||||
Allow users to configure which AI provider/model to use for each distinct phase of the application. This gives users fine-grained control over cost, speed, and quality tradeoffs.
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Existing Settings Fields (UNUSED)
|
||||
|
||||
These fields already exist in `GlobalSettings` but are not wired up:
|
||||
|
||||
```typescript
|
||||
// libs/types/src/settenhancementModel: AgentModel; // Currently ignored, hardcoded to 'sonnet'
|
||||
validationModel: AgentModel; // Currently ignored, hardcoded to 'opus'
|
||||
```
|
||||
|
||||
### All AI Usage Phases
|
||||
|
||||
| Phase | Location | Current Model | Priority |
|
||||
| ------------------- | -------------------------------- | ------------------ | -------- |
|
||||
| Feature Execution | `auto-mode-service.ts` | Per-feature | ✅ Done |
|
||||
| Enhancement | `enhance.ts` | Hardcoded `sonnet` | ✅ Done |
|
||||
| GitHub Validation | `validate-issue.ts` | Hardcoded `opus` | ✅ Done |
|
||||
| File Description | `describe-file.ts` | Hardcoded `haiku` | P2 |
|
||||
| Image Description | `describe-image.ts` | Hardcoded `haiku` | P2 |
|
||||
| App Spec Generation | `generate-spec.ts` | SDK default | P2 |
|
||||
| Feature from Spec | `generate-features-from-spec.ts` | SDK default | P3 |
|
||||
| Backlog Planning | `generate-plan.ts` | SDK default | P3 |
|
||||
| Project Analysis | `analyze-project.ts` | Hardcoded default | P3 |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Type Definitions & Settings Structure
|
||||
|
||||
**Effort**: 2-3 hours
|
||||
**Files**: `libs/types/src/settings.ts`
|
||||
|
||||
### 1.1 Add PhaseModelConfig Type
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Configuration for AI models used in different application phases
|
||||
*/
|
||||
export interface PhaseModelConfig {
|
||||
// Quick tasks - recommend fast/cheap models (Haiku, Cursor auto)
|
||||
enhancementModel: AgentModel | CursorModelId;
|
||||
fileDescriptionModel: AgentModel | CursorModelId;
|
||||
imageDescriptionModel: AgentModel | CursorModelId;
|
||||
|
||||
// Validation tasks - recommend smart models (Sonnet, Opus)
|
||||
validationModel: AgentModel | CursorModelId;
|
||||
|
||||
// Generation tasks - recommend powerful models (Opus, Sonnet)
|
||||
specGenerationModel: AgentModel | CursorModelId;
|
||||
featureGenerationModel: AgentModel | CursorModelId;
|
||||
backlogPlanningModel: AgentModel | CursorModelId;
|
||||
projectAnalysisModel: AgentModel | CursorModelId;
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Update GlobalSettings
|
||||
|
||||
```typescript
|
||||
export interface GlobalSettings {
|
||||
// ... existing fields ...
|
||||
|
||||
// Phase-specific model configuration
|
||||
phaseModels: PhaseModelConfig;
|
||||
|
||||
// Legacy fields (keep for backwards compatibility)
|
||||
enhancementModel?: AgentModel; // Deprecated, use phaseModels
|
||||
validationModel?: AgentModel; // Deprecated, use phaseModels
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 Default Values
|
||||
|
||||
```typescript
|
||||
export const DEFAULT_PHASE_MODELS: PhaseModelConfig = {
|
||||
// Quick tasks - use fast models
|
||||
enhancementModel: 'sonnet',
|
||||
fileDescriptionModel: 'haiku',
|
||||
imageDescriptionModel: 'haiku',
|
||||
|
||||
// Validation - use smart models
|
||||
validationModel: 'sonnet',
|
||||
|
||||
// Generation - use powerful models
|
||||
specGenerationModel: 'opus',
|
||||
featureGenerationModel: 'sonnet',
|
||||
backlogPlanningModel: 'sonnet',
|
||||
projectAnalysisModel: 'sonnet',
|
||||
};
|
||||
```
|
||||
|
||||
### 1.4 Migration Helper
|
||||
|
||||
```typescript
|
||||
// In settings-service.ts
|
||||
function migrateSettings(settings: GlobalSettings): GlobalSettings {
|
||||
// Migrate legacy fields to new structure
|
||||
if (!settings.phaseModels) {
|
||||
settings.phaseModels = {
|
||||
...DEFAULT_PHASE_MODELS,
|
||||
enhancementModel: settings.enhancementModel || 'sonnet',
|
||||
validationModel: settings.validationModel || 'opus',
|
||||
};
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Settings UI
|
||||
|
||||
**Effort**: 6-8 hours
|
||||
**Files**:
|
||||
|
||||
- `apps/ui/src/components/views/settings-view.tsx`
|
||||
- `apps/ui/src/components/views/settings-view/phase-models-tab.tsx` (new)
|
||||
|
||||
### 2.1 Create PhaseModelsTab Component
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AI Phase Configuration │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Configure which AI model to use for each application task. │
|
||||
│ Cursor models require cursor-agent CLI installed. │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ QUICK TASKS │ │
|
||||
│ │ Fast models recommended for speed and cost savings │ │
|
||||
│ ├─────────────────────────────────────────────────────┤ │
|
||||
│ │ │ │
|
||||
│ │ Feature Enhancement │ │
|
||||
│ │ Improves feature names and descriptions │ │
|
||||
│ │ [Claude Sonnet ▼] [Haiku] [Cursor Auto] │ │
|
||||
│ │ │ │
|
||||
│ │ File Descriptions │ │
|
||||
│ │ Generates descriptions for context files │ │
|
||||
│ │ [Claude Haiku ▼] [Sonnet] [Cursor Auto] │ │
|
||||
│ │ │ │
|
||||
│ │ Image Descriptions │ │
|
||||
│ │ Analyzes and describes context images │ │
|
||||
│ │ [Claude Haiku ▼] [Sonnet] [Cursor Auto] │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ VALIDATION TASKS │ │
|
||||
│ │ Smart models recommended for accuracy │ │
|
||||
│ ├─────────────────────────────────────────────────────┤ │
|
||||
│ │ │ │
|
||||
│ │ GitHub Issue Validation │ │
|
||||
│ │ Validates and improves GitHub issues │ │
|
||||
│ │ [Claude Sonnet ▼] [Opus] [Cursor Sonnet] │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ GENERATION TASKS │ │
|
||||
│ │ Powerful models recommended for quality │ │
|
||||
│ ├─────────────────────────────────────────────────────┤ │
|
||||
│ │ │ │
|
||||
│ │ App Specification │ │
|
||||
│ │ Generates full application specifications │ │
|
||||
│ │ [Claude Opus ▼] [Sonnet] [Cursor Opus] │ │
|
||||
│ │ │ │
|
||||
│ │ Feature Generation │ │
|
||||
│ │ Creates features from specifications │ │
|
||||
│ │ [Claude Sonnet ▼] [Opus] [Cursor Auto] │ │
|
||||
│ │ │ │
|
||||
│ │ Backlog Planning │ │
|
||||
│ │ Reorganizes and prioritizes backlog │ │
|
||||
│ │ [Claude Sonnet ▼] [Opus] [Cursor Auto] │ │
|
||||
│ │ │ │
|
||||
│ │ Project Analysis │ │
|
||||
│ │ Analyzes project structure for suggestions │ │
|
||||
│ │ [Claude Sonnet ▼] [Opus] [Cursor Auto] │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Reset to Defaults] [Save Changes] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 PhaseModelSelector Component
|
||||
|
||||
Reusable component for selecting model per phase:
|
||||
|
||||
```typescript
|
||||
interface PhaseModelSelectorProps {
|
||||
phase: keyof PhaseModelConfig;
|
||||
label: string;
|
||||
description: string;
|
||||
value: AgentModel | CursorModelId;
|
||||
onChange: (value: AgentModel | CursorModelId) => void;
|
||||
recommendedModels?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
Features:
|
||||
|
||||
- Shows both Claude and Cursor models
|
||||
- Indicates which provider each model uses
|
||||
- Shows "Recommended" badge on suggested models
|
||||
- Disables Cursor models if CLI not installed
|
||||
- Shows thinking level indicator for supported models
|
||||
|
||||
### 2.3 Add Tab to Settings View
|
||||
|
||||
```typescript
|
||||
// In settings-view.tsx, add new tab
|
||||
const SETTINGS_TABS = [
|
||||
{ id: 'general', label: 'General', icon: Settings },
|
||||
{ id: 'ai-profiles', label: 'AI Profiles', icon: Bot },
|
||||
{ id: 'phase-models', label: 'Phase Models', icon: Workflow }, // NEW
|
||||
{ id: 'providers', label: 'Providers', icon: Key },
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Wire Enhancement Route (P1)
|
||||
|
||||
**Effort**: 2-3 hours
|
||||
**Files**: `apps/server/src/routes/enhance-prompt/routes/enhance.ts`
|
||||
|
||||
### 3.1 Current Code
|
||||
|
||||
```typescript
|
||||
// BEFORE - Hardcoded
|
||||
const model = CLAUDE_MODEL_MAP.sonnet;
|
||||
```
|
||||
|
||||
### 3.2 Updated Code
|
||||
|
||||
```typescript
|
||||
// AFTER - Uses settings
|
||||
import { SettingsService } from '@/services/settings-service.js';
|
||||
import { ProviderFactory } from '@/providers/provider-factory.js';
|
||||
|
||||
const settingsService = new SettingsService(dataDir);
|
||||
const settings = await settingsService.getSettings();
|
||||
const modelId = settings.phaseModels?.enhancementModel || 'sonnet';
|
||||
|
||||
// Resolve to full model string
|
||||
const provider = ProviderFactory.getProviderForModel(modelId);
|
||||
const model = resolveModelString(modelId);
|
||||
```
|
||||
|
||||
### 3.3 Test Cases
|
||||
|
||||
- [ ] Default behavior (uses sonnet) still works
|
||||
- [ ] Changing to haiku in settings uses haiku
|
||||
- [ ] Changing to cursor-auto routes to Cursor provider
|
||||
- [ ] Invalid model falls back to default
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Wire Validation Route (P1)
|
||||
|
||||
**Effort**: 2-3 hours
|
||||
**Files**: `apps/server/src/routes/github/routes/validate-issue.ts`
|
||||
|
||||
### 4.1 Current Code
|
||||
|
||||
```typescript
|
||||
// BEFORE - Has model param but defaults to hardcoded
|
||||
const model = request.body.model || 'opus';
|
||||
```
|
||||
|
||||
### 4.2 Updated Code
|
||||
|
||||
```typescript
|
||||
// AFTER - Uses settings as default
|
||||
const settings = await settingsService.getSettings();
|
||||
const defaultModel = settings.phaseModels?.validationModel || 'opus';
|
||||
const model = request.body.model || defaultModel;
|
||||
```
|
||||
|
||||
### 4.3 Test Cases
|
||||
|
||||
- [ ] Default uses configured model from settings
|
||||
- [ ] Explicit model in request overrides settings
|
||||
- [ ] Cursor models work for validation
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Wire Context Description Routes (P2)
|
||||
|
||||
**Effort**: 3-4 hours
|
||||
**Files**:
|
||||
|
||||
- `apps/server/src/routes/context/routes/describe-file.ts`
|
||||
- `apps/server/src/routes/context/routes/describe-image.ts`
|
||||
|
||||
### 5.1 Pattern
|
||||
|
||||
Same pattern as enhancement - replace hardcoded `haiku` with settings lookup.
|
||||
|
||||
### 5.2 Test Cases
|
||||
|
||||
- [ ] File description uses configured model
|
||||
- [ ] Image description uses configured model (with vision support check)
|
||||
- [ ] Fallback to haiku if model doesn't support vision
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Wire Generation Routes (P2)
|
||||
|
||||
**Effort**: 4-5 hours
|
||||
**Files**:
|
||||
|
||||
- `apps/server/src/routes/app-spec/generate-spec.ts`
|
||||
- `apps/server/src/routes/app-spec/generate-features-from-spec.ts`
|
||||
|
||||
### 6.1 Pattern
|
||||
|
||||
These routes use the Claude SDK directly. Need to:
|
||||
|
||||
1. Load settings
|
||||
2. Resolve model string
|
||||
3. Pass to SDK configuration
|
||||
|
||||
### 6.2 Test Cases
|
||||
|
||||
- [ ] App spec generation uses configured model
|
||||
- [ ] Feature generation uses configured model
|
||||
- [ ] Works with both Claude and Cursor providers
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Wire Remaining Routes (P3)
|
||||
|
||||
**Effort**: 4-5 hours
|
||||
**Files**:
|
||||
|
||||
- `apps/server/src/routes/backlog-plan/generate-plan.ts`
|
||||
- `apps/server/src/routes/auto-mode/routes/analyze-project.ts`
|
||||
|
||||
### 7.1 Pattern
|
||||
|
||||
Same settings injection pattern.
|
||||
|
||||
### 7.2 Test Cases
|
||||
|
||||
- [ ] Backlog planning uses configured model
|
||||
- [ ] Project analysis uses configured model
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
```
|
||||
Phase 1: Types & Settings Structure
|
||||
Phase 2: Settings UI (Phase Models tab)
|
||||
Phase 8: Quick Model Override Component <- Right after UI for easier testing
|
||||
Phase 9: Integration Points for Override <- Wire override to each feature
|
||||
|
||||
Then wire routes (testing both global + override together):
|
||||
Phase 3: Enhancement Route - Done
|
||||
Phase 4: Validation Route - Broken validation parsing ! need urgent fix to properly validate cursor cli output
|
||||
Phase 5: Context Routes (file/image description)
|
||||
Phase 6: Generation Routes (spec, features)
|
||||
Phase 7: Remaining Routes (backlog, analysis)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
### New Files
|
||||
|
||||
- `apps/ui/src/components/views/settings-view/phase-models-tab.tsx`
|
||||
- `apps/ui/src/components/views/settings-view/phase-model-selector.tsx`
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Changes |
|
||||
| ---------------------------------------------------------------- | ------------------------- |
|
||||
| `libs/types/src/settings.ts` | Add PhaseModelConfig type |
|
||||
| `apps/server/src/services/settings-service.ts` | Add migration logic |
|
||||
| `apps/ui/src/components/views/settings-view.tsx` | Add Phase Models tab |
|
||||
| `apps/server/src/routes/enhance-prompt/routes/enhance.ts` | Use settings |
|
||||
| `apps/server/src/routes/github/routes/validate-issue.ts` | Use settings |
|
||||
| `apps/server/src/routes/context/routes/describe-file.ts` | Use settings |
|
||||
| `apps/server/src/routes/context/routes/describe-image.ts` | Use settings |
|
||||
| `apps/server/src/routes/app-spec/generate-spec.ts` | Use settings |
|
||||
| `apps/server/src/routes/app-spec/generate-features-from-spec.ts` | Use settings |
|
||||
| `apps/server/src/routes/backlog-plan/generate-plan.ts` | Use settings |
|
||||
| `apps/server/src/routes/auto-mode/routes/analyze-project.ts` | Use settings |
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Settings migration preserves existing values
|
||||
- Default values applied correctly
|
||||
- Model resolution works for both providers
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Each phase uses configured model
|
||||
- Provider factory routes correctly
|
||||
- Cursor fallback when CLI not available
|
||||
|
||||
### E2E Tests
|
||||
|
||||
- Settings UI saves correctly
|
||||
- Changes persist across restarts
|
||||
- Each feature works with non-default model
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise:
|
||||
|
||||
1. All routes have fallback to hardcoded defaults
|
||||
2. Settings migration is additive (doesn't remove old fields)
|
||||
3. Can revert individual routes independently
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] Users can configure model for each phase via Settings UI
|
||||
- [ ] All 8+ phases respect configured model
|
||||
- [ ] Cursor models work for all applicable phases
|
||||
- [ ] Graceful fallback when Cursor CLI not available
|
||||
- [ ] Settings persist across app restarts
|
||||
- [ ] No regression in existing functionality
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Quick Model Override Component (P1)
|
||||
|
||||
**Effort**: 4-6 hours
|
||||
**Files**:
|
||||
|
||||
- `apps/ui/src/components/shared/model-override-popover.tsx` (new)
|
||||
- `apps/ui/src/components/shared/model-override-trigger.tsx` (new)
|
||||
|
||||
### 8.1 Concept
|
||||
|
||||
Global defaults are great, but users often want to override for a specific run:
|
||||
|
||||
- "Use Opus for this complex feature"
|
||||
- "Use Cursor for this quick fix"
|
||||
- "Use Haiku to save costs on this simple task"
|
||||
|
||||
### 8.2 Component: ModelOverrideTrigger
|
||||
|
||||
A small gear/settings icon that opens the override popover:
|
||||
|
||||
```typescript
|
||||
interface ModelOverrideTriggerProps {
|
||||
// Current effective model (from global settings or explicit override)
|
||||
currentModel: string;
|
||||
|
||||
// Callback when user selects override
|
||||
onModelChange: (model: string | null) => void;
|
||||
|
||||
// Optional: which phase this is for (shows recommended models)
|
||||
phase?: keyof PhaseModelConfig;
|
||||
|
||||
// Size variants for different contexts
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
|
||||
// Show as icon-only or with label
|
||||
variant?: 'icon' | 'button' | 'inline';
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Component: ModelOverridePopover
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Model Override [x] │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Current: Claude Sonnet (from settings) │
|
||||
│ │
|
||||
│ ○ Use Global Setting │
|
||||
│ └─ Claude Sonnet │
|
||||
│ │
|
||||
│ ● Override for this run: │
|
||||
│ │
|
||||
│ CLAUDE │
|
||||
│ ┌──────┐ ┌──────┐ ┌───────┐ │
|
||||
│ │ Opus │ │Sonnet│ │ Haiku │ │
|
||||
│ └──────┘ └──────┘ └───────┘ │
|
||||
│ │
|
||||
│ CURSOR │
|
||||
│ ┌──────┐ ┌────────┐ ┌───────┐ │
|
||||
│ │ Auto │ │Sonnet45│ │GPT-5.2│ │
|
||||
│ └──────┘ └────────┘ └───────┘ │
|
||||
│ │
|
||||
│ [Clear Override] [Apply] │
|
||||
│ │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 8.4 Usage Examples
|
||||
|
||||
**In Feature Modal (existing model selector enhancement):**
|
||||
|
||||
```tsx
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>Model</Label>
|
||||
<ModelOverrideTrigger
|
||||
currentModel={feature.model || globalDefault}
|
||||
onModelChange={(model) => setFeature({ ...feature, model })}
|
||||
phase="featureExecution"
|
||||
size="md"
|
||||
variant="button"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
**In Kanban Card Actions:**
|
||||
|
||||
```tsx
|
||||
<CardActions>
|
||||
<Button onClick={handleImplement}>Implement</Button>
|
||||
<ModelOverrideTrigger
|
||||
currentModel={feature.model}
|
||||
onModelChange={handleQuickModelChange}
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
</CardActions>
|
||||
```
|
||||
|
||||
**In Enhancement Dialog:**
|
||||
|
||||
```tsx
|
||||
<DialogHeader>
|
||||
<DialogTitle>Enhance Feature</DialogTitle>
|
||||
<ModelOverrideTrigger
|
||||
currentModel={settings.phaseModels.enhancementModel}
|
||||
onModelChange={setEnhanceModel}
|
||||
phase="enhancement"
|
||||
size="sm"
|
||||
variant="icon"
|
||||
/>
|
||||
</DialogHeader>
|
||||
```
|
||||
|
||||
**In GitHub Issue Import:**
|
||||
|
||||
```tsx
|
||||
<div className="flex justify-between">
|
||||
<span>Validating issue...</span>
|
||||
<ModelOverrideTrigger
|
||||
currentModel={validationModel}
|
||||
onModelChange={setValidationModel}
|
||||
phase="validation"
|
||||
size="sm"
|
||||
variant="inline"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 8.5 Visual Variants
|
||||
|
||||
```
|
||||
Icon Only (size=sm):
|
||||
┌───┐
|
||||
│ ⚙ │ <- Just gear icon, hover shows current model
|
||||
└───┘
|
||||
|
||||
Button (size=md):
|
||||
┌─────────────────┐
|
||||
│ ⚙ Claude Sonnet │ <- Gear + model name
|
||||
└─────────────────┘
|
||||
|
||||
Inline (size=sm):
|
||||
Using Claude Sonnet ⚙ <- Text with gear at end
|
||||
```
|
||||
|
||||
### 8.6 State Management
|
||||
|
||||
```typescript
|
||||
// Hook for managing model overrides
|
||||
function useModelOverride(phase: keyof PhaseModelConfig) {
|
||||
const { settings } = useSettings();
|
||||
const [override, setOverride] = useState<string | null>(null);
|
||||
|
||||
const effectiveModel = override || settings.phaseModels[phase];
|
||||
const isOverridden = override !== null;
|
||||
|
||||
const clearOverride = () => setOverride(null);
|
||||
|
||||
return {
|
||||
effectiveModel,
|
||||
isOverridden,
|
||||
setOverride,
|
||||
clearOverride,
|
||||
globalDefault: settings.phaseModels[phase],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 8.7 Visual Feedback for Overrides
|
||||
|
||||
When a model is overridden from global:
|
||||
|
||||
- Show small indicator dot on the gear icon
|
||||
- Different color tint on the trigger
|
||||
- Tooltip shows "Overridden from global setting"
|
||||
|
||||
```tsx
|
||||
// Indicator when overridden
|
||||
<div className="relative">
|
||||
<GearIcon />
|
||||
{isOverridden && <div className="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full" />}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Integration Points for Quick Override
|
||||
|
||||
**Effort**: 3-4 hours
|
||||
|
||||
### 9.1 Feature Modal
|
||||
|
||||
**File**: `apps/ui/src/components/views/board-view/components/feature-modal.tsx`
|
||||
|
||||
Replace current model selector with ModelOverrideTrigger:
|
||||
|
||||
- Shows inherited model from AI Profile
|
||||
- Allows quick override for this feature
|
||||
- Clear override returns to profile default
|
||||
|
||||
### 9.2 Kanban Card
|
||||
|
||||
**File**: `apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx`
|
||||
|
||||
Add small gear icon next to "Implement" button:
|
||||
|
||||
- Quick model change before running
|
||||
- Doesn't persist to feature (one-time override)
|
||||
|
||||
### 9.3 Enhancement Dialog
|
||||
|
||||
**File**: `apps/ui/src/components/views/board-view/components/enhance-dialog.tsx`
|
||||
|
||||
Add override trigger in header:
|
||||
|
||||
- Default from global settings
|
||||
- Override for this enhancement only
|
||||
|
||||
### 9.4 GitHub Import
|
||||
|
||||
**File**: `apps/ui/src/components/views/github-view/`
|
||||
|
||||
Add override for validation model:
|
||||
|
||||
for this feature
|
||||
|
||||
- Clear override returns to profile default
|
||||
|
||||
### 9.2 Kanban Card
|
||||
|
||||
**File**: `apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx`
|
||||
|
||||
Add small gear icon next to "Implement" button:
|
||||
|
||||
- Quick model change before running
|
||||
- Doesn't persist to feature (one-time override)
|
||||
|
||||
### 9.3 Enhancement Dialog
|
||||
|
||||
**File**: `apps/ui/src/components/views/board-view/components/enhance-dialog.tsx`
|
||||
|
||||
Add override trigger in header:
|
||||
|
||||
- Default from global settings
|
||||
- Override for this enhancement only
|
||||
|
||||
### 9.4 GitHub Import
|
||||
|
||||
**File**: `apps/ui/src/components/views/github-view/`
|
||||
|
||||
Add override for validation model:
|
||||
|
||||
- Default from global settings
|
||||
- Override for this import session
|
||||
|
||||
---
|
||||
|
||||
## Updated Implementation Order
|
||||
|
||||
```
|
||||
FOUNDATION:
|
||||
├── Phase 1: Types & Settings Structure
|
||||
├── Phase 2: Settings UI (Phase Models tab)
|
||||
├── Phase 8: Quick Model Override Component
|
||||
└── Phase 9: Integration Points (wire override to feature modal, kanban, etc.)
|
||||
|
||||
ROUTE WIRING (test both global settings + quick override for each):
|
||||
├── Phase 3: Enhancement Route + Test global + override
|
||||
├── Phase 4: Validation Route + Test global + override
|
||||
├── Phase 5: Context Routes + Test global + override
|
||||
├── Phase 6: Generation Routes + Test global + override
|
||||
└── Phase 7: Remaining Routes + Test global + override
|
||||
|
||||
FINALIZATION:
|
||||
├── Full Integration Testing
|
||||
└── Documentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture: Global vs Override
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Settings Hierarchy │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Level 1: Global Defaults (DEFAULT_PHASE_MODELS) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Level 2: User Global Settings (settings.phaseModels) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Level 3: Feature-Level Override (feature.model) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ Level 4: Run-Time Override (via ModelOverridePopover) │
|
||||
│ │
|
||||
│ Resolution: First non-null value wins (bottom-up) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
```typescript
|
||||
function resolveModel(
|
||||
phase: keyof PhaseModelConfig,
|
||||
feature?: Feature,
|
||||
runtimeOverride?: string
|
||||
): string {
|
||||
// Runtime override takes precedence
|
||||
if (runtimeOverride) return runtimeOverride;
|
||||
|
||||
// Feature-level override
|
||||
if (feature?.model) return feature.model;
|
||||
|
||||
// User global settings
|
||||
const settings = getSettings();
|
||||
if (settings.phaseModels?.[phase]) return settings.phaseModels[phase];
|
||||
|
||||
// Default
|
||||
return DEFAULT_PHASE_MODELS[phase];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Per-Project Overrides**: Allow project-level phase model config
|
||||
2. **Quick Presets**: "Cost Optimized", "Quality First", "Balanced" presets
|
||||
3. **Usage Stats**: Show which models used for which phases
|
||||
4. **Auto-Selection**: ML-based model selection based on task complexity
|
||||
5. **Model History**: Remember last-used model per phase for quick access
|
||||
6. **Keyboard Shortcuts**: Cmd+Shift+M to quickly change model anywhere
|
||||
Reference in New Issue
Block a user