# Architecture: Cursor Adapter ## Overview Cursor Adapter 是一個本機 HTTP proxy server,將 OpenAI-compatible API 請求轉換為 Cursor CLI headless 模式指令,並將 Cursor CLI 的 streaming JSON 輸出轉換為 OpenAI SSE 格式回傳給 CLI 工具。 核心架構:**單一 binary、無狀態、spawn 子程序模式**。 ### Requirement Traceability | PRD Requirement | Architectural Component | |----------------|------------------------| | FR1: OpenAI-compatible API | HTTP Server (net/http + chi router) | | FR2: Cursor CLI Integration | CLI Bridge (os/exec subprocess) | | FR3: Streaming Response Conversion | Stream Converter (goroutine pipeline) | | FR4: Model Listing | Model Registry | | FR5: Configuration | Config Module (YAML) | | FR6: Error Handling | Error Handler (middleware) | | NFR1: Performance < 500ms overhead | goroutine pipeline, zero-copy streaming | | NFR2: Concurrent requests ≤ 5 | semaphore (buffered channel) | ## System Architecture ### Technology Stack | Layer | Technology | Justification | |-------|-----------|---------------| | Language | Go 1.22+ | 單一 binary、subprocess 管理好、goroutine 天然適合 streaming | | HTTP Router | go-chi/chi v5 | 輕量、相容 net/http、middleware 支援好 | | Config | gopkg.in/yaml.v3 | 標準 YAML 解析 | | CLI | spf13/cobra | Go 標準 CLI 框架 | | Testing | testify + stdlib | table-driven test + assertion helper | ### Component Architecture ``` ┌─────────────────────────────────────────────────┐ │ cursor-adapter (single binary) │ │ │ │ cmd/ │ │ └── cursor-adapter/main.go (cobra entrypoint) │ │ │ │ internal/ │ │ ├── server/ HTTP Server (chi router) │ │ │ ├── handler.go route handlers │ │ │ ├── middleware.go error + logging │ │ │ └── sse.go SSE writer helpers │ │ ├── bridge/ CLI Bridge │ │ │ ├── bridge.go spawn subprocess │ │ │ └── scanner.go stdout line reader │ │ ├── converter/ Stream Converter │ │ │ └── convert.go cursor-json → OpenAI SSE │ │ └── config/ Config Module │ │ └── config.go YAML loading + defaults │ │ │ └─────────────────────────────────────────────────┘ │ │ os/exec.CommandContext ▼ ┌──────────────────┐ │ Cursor CLI │ │ agent -p ... │ │ --model ... │ │ --output-format│ │ stream-json │ └──────────────────┘ ``` ## Service Boundaries 單一 binary,4 個 internal package: | Package | Responsibility | Exported | |---------|---------------|----------| | cmd/cursor-adapter | CLI 入口、wiring | main() | | internal/server | HTTP routes + middleware | NewServer(), Server.Run() | | internal/bridge | spawn/manage Cursor CLI subprocess | Bridge interface + CLIBridge | | internal/converter | stream-json → OpenAI SSE 轉換 | Convert() functions | | internal/config | YAML config 載入/驗證 | Config struct, Load() | ### Communication Matrix | From | To | Pattern | Purpose | |------|----|---------|---------| | server/handler | bridge | interface call | 啟動子程序 | | bridge | converter | channel (chan string) | 逐行傳遞 stdout | | converter | server/handler | channel (chan SSEChunk) | 回傳轉換後的 chunk | | server/handler | client | HTTP SSE | 回傳給 CLI 工具 | ## Data Flow ### Chat Completion (Streaming) ``` 1. Client → POST /v1/chat/completions (stream: true) 2. handler → 驗證 request body 3. handler → 從 messages[] 組合 prompt 4. bridge → ctx, cancel := context.WithTimeout(...) 5. bridge → cmd := exec.CommandContext(ctx, "agent", "-p", prompt, "--model", model, "--output-format", "stream-json") 6. bridge → cmd.Stdout pipe → goroutine scanner 逐行讀 7. scanner → 每行送入 outputChan (chan string) 8. converter → 讀 outputChan,轉換為 SSEChunk,送入 sseChan 9. handler → flush SSE chunk 到 client 10. bridge → process 結束 → close channels → handler 發送 [DONE] ``` ## Database Schema N/A。無狀態設計,不需要資料庫。 ## API Contract ### POST /v1/chat/completions Request: ```json { "model": "claude-sonnet-4-20250514", "messages": [ {"role": "user", "content": "hello"} ], "stream": true } ``` Response (SSE when stream: true): ``` data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} data: [DONE] ``` Response (JSON when stream: false): ```json { "id": "chatcmpl-xxx", "object": "chat.completion", "choices": [{"index": 0, "message": {"role": "assistant", "content": "Hello!"}, "finish_reason": "stop"}], "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0} } ``` ### GET /v1/models Response: ```json { "object": "list", "data": [ {"id": "claude-sonnet-4-20250514", "object": "model", "created": 0, "owned_by": "cursor"} ] } ``` ### GET /health Response: ```json {"status": "ok", "cursor_cli": "available", "version": "0.1.0"} ``` ### Error Codes | Status | Code | When | |--------|------|------| | 400 | invalid_request | messages 為空或格式錯誤 | | 404 | model_not_found | 指定的 model 不存在 | | 500 | internal_error | Cursor CLI 子程序崩潰 | | 504 | timeout | Cursor CLI 超時未回應 | ## Async / Queue Design N/A。不需要 queue,goroutine + channel 直接串接。 ## Consistency Model N/A。無狀態 proxy,每次請求獨立。 ## Error Model | Category | Examples | Handling | |----------|---------|----------| | Client Error | invalid request, unknown model | 4xx,不 spawn 子程序 | | CLI Spawn Error | agent not found, not logged in | 500 + stderr message | | Timeout | model thinking too long | kill subprocess → 504 | | Crash | unexpected exit | 500 + exit code | ## Security Boundaries N/A。本機 personal tool,bind 127.0.0.1,無認證。 ## Integration Boundaries ### Cursor CLI | Property | Value | |----------|-------| | Integration Pattern | subprocess (os/exec.CommandContext) | | Protocol | CLI binary (stdout pipe) | | Authentication | 本機 `agent login` 狀態 | | Failure Mode | binary not found / not logged in | | Data Contract | `--output-format stream-json` | | Timeout | 可配置,預設 300s | ## Observability - structlog (slog) logging:INFO 請求/完成、ERROR 錯誤/timeout - `/health` endpoint - DEBUG level 時印出 Cursor CLI 原始 stdout ## Scaling Strategy N/A。個人本機工具,單實例。semaphore 限制並發子程序數(預設 5)。 ## Non-Functional Requirements | NFR | Requirement | Decision | Verification | |-----|-------------|----------|-------------| | Performance | overhead < 500ms | goroutine pipeline, streaming pipe | 實際測量 | | Reliability | 並發 ≤ 5 | buffered channel semaphore | 併發測試 | | Usability | 一行啟動 | cobra CLI, sensible defaults | 手動測試 | | Distribution | 單一 binary | Go cross-compile | `go build` | ## Mermaid Diagrams ### System Architecture ```mermaid graph LR CLI[Hermes/OpenCode/Claude] -->|POST /v1/chat/completions| Adapter[Cursor Adapter] Adapter -->|exec: agent -p ... --output-format stream-json| Cursor[Cursor CLI] Cursor -->|streaming JSON stdout| Adapter Adapter -->|SSE streaming| CLI ``` ### Sequence Diagram ```mermaid sequenceDiagram participant C as CLI Tool participant H as HTTP Handler participant B as CLI Bridge participant A as Cursor CLI C->>H: POST /v1/chat/completions H->>H: validate, extract prompt H->>B: Execute(ctx, prompt, model) B->>A: exec.CommandContext("agent", "-p", ...) loop streaming A-->>B: stdout line (JSON) B-->>H: outputChan <- line H->>H: convert to SSE chunk H-->>C: data: {...}\n\n end A-->>B: process exit B-->>H: close channels H-->>C: data: [DONE] ``` ### Data Flow Diagram ```mermaid flowchart TD A[Client Request] --> B{Validate} B -->|invalid| C[400] B -->|valid| D[Extract Prompt] D --> E[exec.CommandContext] E --> F{spawn OK?} F -->|no| G[500] F -->|yes| H[goroutine: scan stdout] H --> I[outputChan] I --> J[converter: JSON→SSE] J --> K[flush to client] K --> L{more?} L -->|yes| H L -->|no| M[send DONE] ``` ## ADR ### ADR-001: Go 而非 Python **Context**: 選擇實作語言。候選為 Go 和 Python (FastAPI)。 **Decision**: Go 1.22+。 **Consequences**: - + 單一 binary,不需要使用者裝 Python/pip - + `os/exec.CommandContext` 子程序管理比 Python `asyncio` 更直覺 - + goroutine + channel 天然適合 streaming pipeline - + cross-compile,macOS/Linux/Windows 一個 `go build` - - SSE 手動處理(但不複雜) **Alternatives**: - Python + FastAPI:生態好,但需要 runtime,部署麻煩 - Rust:效能最好,但開發速度慢 ### ADR-002: chi router 而非 stdlib mux **Context**: Go 1.22 的 `net/http` 已支援 method-based routing。 **Decision**: 使用 chi v5。 **Consequences**: - + middleware 生態好(logger、recoverer、timeout) - + route grouping 更乾淨 - + 相容 net/http Handler - - 多一個 dependency **Alternatives**: - stdlib net/http:夠用,但 middleware 要自己寫 - gin:太重,對這個規模 overkill ### ADR-003: spawn 子程序而非 ACP **Context**: Cursor CLI 支援 headless print mode 和 ACP (JSON-RPC)。 **Decision**: headless print mode (`agent -p --output-format stream-json`)。 **Consequences**: - + 實作簡單:spawn + 讀 stdout - + 不需要 JSON-RPC - - 無法做 tool use(PRD 不需要) **Alternatives**: - ACP (JSON-RPC over stdio):功能完整,但複雜度高很多 ## Risks | Risk | Impact | Likelihood | Mitigation | |------|--------|-----------|------------| | Cursor CLI stream-json 格式變更 | High | Medium | 抽象 converter,格式在 const 定義 | | Cursor CLI 不支援並發實例 | Medium | Low | semaphore + queue | | 子程序 zombie | Medium | Low | CommandContext + Wait() | ## Open Questions 1. Cursor CLI stream-json 的確切 schema?(需實際測試) 2. Cursor CLI 能否同時跑多個 headless 實例?