diff --git a/plan/cursor-cli-integration/PHASE_PROMPT.md b/plan/cursor-cli-integration/PHASE_PROMPT.md deleted file mode 100644 index 9dd01b1a..00000000 --- a/plan/cursor-cli-integration/PHASE_PROMPT.md +++ /dev/null @@ -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. diff --git a/plan/cursor-cli-integration/README.md b/plan/cursor-cli-integration/README.md deleted file mode 100644 index af814a20..00000000 --- a/plan/cursor-cli-integration/README.md +++ /dev/null @@ -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 - ├── detectInstallation(): Promise - └── 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//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 | diff --git a/plan/cursor-cli-integration/cursor-integration-analysis.md b/plan/cursor-cli-integration/cursor-integration-analysis.md deleted file mode 100644 index c38df8f5..00000000 --- a/plan/cursor-cli-integration/cursor-integration-analysis.md +++ /dev/null @@ -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 ` | `text`, `json`, or `stream-json` | -| `--model ` | Model selection (e.g., `gpt-5`, `sonnet-4`) | -| `--workspace ` | Working directory | -| `--resume [chatId]` | Resume previous session | -| `--api-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 # Authenticate with server -cursor-agent mcp list-tools # List tools for server -cursor-agent mcp disable # 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 = { - 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 diff --git a/plan/cursor-cli-integration/phases/phase-0-analysis.md b/plan/cursor-cli-integration/phases/phase-0-analysis.md deleted file mode 100644 index 6c8e1d0f..00000000 --- a/plan/cursor-cli-integration/phases/phase-0-analysis.md +++ /dev/null @@ -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` -- 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) diff --git a/plan/cursor-cli-integration/phases/phase-1-types.md b/plan/cursor-cli-integration/phases/phase-1-types.md deleted file mode 100644 index d0daff96..00000000 --- a/plan/cursor-cli-integration/phases/phase-1-types.md +++ /dev/null @@ -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 = { - 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 diff --git a/plan/cursor-cli-integration/phases/phase-10-testing.md b/plan/cursor-cli-integration/phases/phase-10-testing.md deleted file mode 100644 index c4841715..00000000 --- a/plan/cursor-cli-integration/phases/phase-10-testing.md +++ /dev/null @@ -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 diff --git a/plan/cursor-cli-integration/phases/phase-2-provider.md b/plan/cursor-cli-integration/phases/phase-2-provider.md deleted file mode 100644 index 13ff4ec9..00000000 --- a/plan/cursor-cli-integration/phases/phase-2-provider.md +++ /dev/null @@ -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//cursor-agent - * - Symlink: ~/.local/bin/cursor-agent -> versions//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 = { - 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 { - return this.cliPath !== null; - } - - /** - * Get Cursor CLI version - */ - async getVersion(): Promise { - 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 { - 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 { - 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 { - 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 diff --git a/plan/cursor-cli-integration/phases/phase-3-factory.md b/plan/cursor-cli-integration/phases/phase-3-factory.md deleted file mode 100644 index 62fa7035..00000000 --- a/plan/cursor-cli-integration/phases/phase-3-factory.md +++ /dev/null @@ -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> { - const providers = this.getAllProviders(); - const statuses: Record = {}; - - 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 diff --git a/plan/cursor-cli-integration/phases/phase-4-routes.md b/plan/cursor-cli-integration/phases/phase-4-routes.md deleted file mode 100644 index bc5eeb71..00000000 --- a/plan/cursor-cli-integration/phases/phase-4-routes.md +++ /dev/null @@ -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 diff --git a/plan/cursor-cli-integration/phases/phase-5-log-parser.md b/plan/cursor-cli-integration/phases/phase-5-log-parser.md deleted file mode 100644 index 7c28da6f..00000000 --- a/plan/cursor-cli-integration/phases/phase-5-log-parser.md +++ /dev/null @@ -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 diff --git a/plan/cursor-cli-integration/phases/phase-6-setup-wizard.md b/plan/cursor-cli-integration/phases/phase-6-setup-wizard.md deleted file mode 100644 index 3e7e00b7..00000000 --- a/plan/cursor-cli-integration/phases/phase-6-setup-wizard.md +++ /dev/null @@ -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(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 ( - - - - - Cursor CLI Setup - - Optional - - - - Configure Cursor CLI as an alternative AI provider. You can skip this and use Claude - instead, or configure it later in Settings. - - - - - {/* Installation Status */} -
-
- CLI Installation - {isChecking ? ( - - ) : status?.installed ? ( -
- - v{status.version} -
- ) : ( -
- - Not installed -
- )} -
- - {!status?.installed && !isChecking && ( - - -

Install Cursor CLI to use Cursor models:

-
- - {status?.installCommand || 'curl https://cursor.com/install -fsS | bash'} - - -
- -
-
- )} -
- - {/* Authentication Status */} - {status?.installed && ( -
-
- Authentication - {status.auth?.authenticated ? ( -
- - - {status.auth.method === 'api_key' ? 'API Key' : 'Browser Login'} - -
- ) : ( -
- - Not authenticated -
- )} -
- - {!status.auth?.authenticated && ( -
-

- Run the login command in your terminal, then complete authentication in your - browser: -

-
- - {status.loginCommand || 'cursor-agent login'} - -
- -
- )} -
- )} - - {/* Action Buttons */} -
- - - -
- - {/* Info note */} -

- You can always configure Cursor later in Settings → Providers -

-
-
- ); -} - -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>(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 ( -
- {/* Progress indicator */} -
- {SETUP_STEPS.map((s, i) => ( -
- ))} -
- - {/* Step title */} -

- {step.title} - {step.optional && (Optional)} -

- - {/* Step component */} - handleSkip(step.id)} /> -
- ); -} -``` - -### 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 diff --git a/plan/cursor-cli-integration/phases/phase-7-settings.md b/plan/cursor-cli-integration/phases/phase-7-settings.md deleted file mode 100644 index 46255fef..00000000 --- a/plan/cursor-cli-integration/phases/phase-7-settings.md +++ /dev/null @@ -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(null); - const [config, setConfig] = useState(null); - const [availableModels, setAvailableModels] = useState([]); - 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 ( -
- -
- ); - } - - return ( -
- {/* Status Card */} - - - - - Cursor CLI Status - - - - {/* Installation */} -
- Installation - {status?.installed ? ( -
- - v{status.version} -
- ) : ( -
- - Not installed -
- )} -
- - {/* Authentication */} -
- Authentication - {status?.authenticated ? ( -
- - - {status.method === 'api_key' ? 'API Key' : 'Browser Login'} - -
- ) : ( -
- - Not authenticated -
- )} -
- - {/* Actions */} -
- - {!status?.installed && ( - - )} -
-
-
- - {/* Model Configuration */} - {status?.installed && config && ( - - - Model Configuration - - Configure which Cursor models are available and set the default - - - - {/* Default Model */} -
- - -
- - {/* Enabled Models */} -
- -
- {availableModels.map((model) => { - const isEnabled = config.models?.includes(model.id) ?? false; - const isAuto = model.id === 'auto'; - - return ( -
-
- handleModelToggle(model.id, !!checked)} - disabled={isSaving || isAuto} - /> -
-
- {model.label} - {model.hasThinking && ( - - Thinking - - )} -
-

{model.description}

-
-
- - {model.tier} - -
- ); - })} -
-
-
-
- )} - - {/* Not Installed State */} - {!status?.installed && ( - - - -

Cursor CLI is not installed.

-

Install it to use Cursor models in AutoMaker.

-
-
- )} -
- ); -} - -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 ( - - - - - Claude - - - - Cursor - - - - - - - - - - - - ); -} - -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(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 ( -
- -
- ); - } - - return ( -
- - - - - Claude Status - - Claude is the primary AI provider for AutoMaker - - -
- SDK Status -
- - Active -
-
- -
- Authentication - {status?.authenticated ? ( -
- - {status.method} -
- ) : ( -
- - Not authenticated -
- )} -
- - -
-
-
- ); -} - -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) diff --git a/plan/cursor-cli-integration/phases/phase-8-profiles.md b/plan/cursor-cli-integration/phases/phase-8-profiles.md deleted file mode 100644 index 69fa900b..00000000 --- a/plan/cursor-cli-integration/phases/phase-8-profiles.md +++ /dev/null @@ -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(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 ( -
- {/* Name & Description */} -
-
- - setFormData((p) => ({ ...p, name: e.target.value }))} - placeholder="My Profile" - /> -
-
- -