diff --git a/docs/.DS_Store b/docs/.DS_Store deleted file mode 100644 index f6896f6..0000000 Binary files a/docs/.DS_Store and /dev/null differ diff --git a/docs/architecture/2026-04-14-cursor-adapter.md b/docs/architecture/2026-04-14-cursor-adapter.md deleted file mode 100644 index 3554d85..0000000 --- a/docs/architecture/2026-04-14-cursor-adapter.md +++ /dev/null @@ -1,343 +0,0 @@ -# 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 實例? diff --git a/docs/code-design/2026-04-14-cursor-adapter.md b/docs/code-design/2026-04-14-cursor-adapter.md deleted file mode 100644 index ea66fc1..0000000 --- a/docs/code-design/2026-04-14-cursor-adapter.md +++ /dev/null @@ -1,456 +0,0 @@ -# Code Design: Cursor Adapter - -## Overview - -將架構文件轉為 Go 程式碼層級設計。基於架構文件 `docs/architecture/2026-04-14-cursor-adapter.md`。 - -語言:Go 1.22+。遵循 `language-go` skill 的設計規範。 - -## Project Structure - -``` -cursor-adapter/ -├── go.mod -├── go.sum -├── main.go # cobra CLI 入口 + wiring -├── config.example.yaml -├── Makefile -├── README.md -├── internal/ -│ ├── config/ -│ │ └── config.go # Config struct + Load() -│ ├── bridge/ -│ │ ├── bridge.go # CLIBridge struct + methods -│ │ └── bridge_test.go -│ ├── converter/ -│ │ ├── convert.go # Cursor JSON → OpenAI SSE 轉換 -│ │ └── convert_test.go -│ └── server/ -│ ├── server.go # chi router + handler wiring -│ ├── handler.go # route handler functions -│ ├── handler_test.go -│ ├── sse.go # SSE write helpers -│ └── models.go # request/response structs (JSON) -└── scripts/ - └── test_cursor_cli.sh # 探索性測試 Cursor CLI 輸出 -``` - -### Package Responsibilities - -| Package | Responsibility | Exports | -|---------|---------------|---------| -| main (root) | CLI 入口、wiring | main() | -| internal/config | YAML config 載入 + 預設值 | Config, Load() | -| internal/bridge | spawn/manage Cursor CLI subprocess | Bridge interface, CLIBridge | -| internal/converter | stream-json → OpenAI SSE 轉換 | ToOpenAIChunk(), ToOpenAIResponse() | -| internal/server | HTTP routes, handlers, SSE | New(), Server.Run() | - -## Layer Architecture - -``` -main.go (wiring) - ↓ 建立 config, bridge, server -internal/server (HTTP layer) - ↓ handler 呼叫 bridge -internal/bridge (CLI layer) - ↓ spawn subprocess, 讀 stdout -internal/converter (轉換 layer) - ↓ JSON 轉 SSE -Cursor CLI (外部) -``` - -## Interface Definitions - -```go -// internal/bridge/bridge.go - -// Bridge 定義與外部 CLI 工具的整合介面。 -type Bridge interface { - // Execute 執行 prompt,透過 channel 逐行回傳 Cursor CLI 的 stdout。 - // context timeout 時會 kill subprocess。 - Execute(ctx context.Context, prompt string, model string) (<-chan string, <-chan error) - - // ListModels 回傳可用的模型列表。 - ListModels(ctx context.Context) ([]string, error) - - // CheckHealth 確認 Cursor CLI 是否可用。 - CheckHealth(ctx context.Context) error -} - -// CLIBridge 實作 Bridge,透過 os/exec spawn Cursor CLI。 -type CLIBridge struct { - cursorPath string // "agent" 或 config 指定的路徑 - semaphore chan struct{} // 限制並發數 - timeout time.Duration -} - -func NewCLIBridge(cursorPath string, maxConcurrent int, timeout time.Duration) *CLIBridge - -func (b *CLIBridge) Execute(ctx context.Context, prompt string, model string) (<-chan string, <-chan error) -func (b *CLIBridge) ListModels(ctx context.Context) ([]string, error) -func (b *CLIBridge) CheckHealth(ctx context.Context) error -``` - -```go -// internal/converter/convert.go - -// CursorLine 代表 Cursor CLI stream-json 的一行。 -type CursorLine struct { - Type string `json:"type"` // "assistant", "result", "error", etc. - Content string `json:"content"` // 文字內容(type=assistant 時) -} - -// ToOpenAIChunk 將一行 Cursor JSON 轉換為 OpenAI SSE chunk struct。 -// 實際 JSON schema 需等 P2 探索確認後定義。 -func ToOpenAIChunk(line string, chatID string) (*OpenAIChunk, error) - -// ToOpenAIResponse 將多行 Cursor output 組合為完整 response。 -func ToOpenAIResponse(lines []string, chatID string) (*OpenAIResponse, error) - -// SSE 格式化 -func FormatSSE(data any) string // "data: {json}\n\n" -func FormatDone() string // "data: [DONE]\n\n" -``` - -```go -// internal/config/config.go - -type Config struct { - Port int `yaml:"port"` - CursorCLIPath string `yaml:"cursor_cli_path"` - DefaultModel string `yaml:"default_model"` - Timeout int `yaml:"timeout"` // seconds - MaxConcurrent int `yaml:"max_concurrent"` - LogLevel string `yaml:"log_level"` - AvailableModels []string `yaml:"available_models"` // optional -} - -// Load 從 YAML 檔載入配置,套用預設值。 -// path 為空時使用預設路徑 ~/.cursor-adapter/config.yaml。 -func Load(path string) (*Config, error) - -// Defaults 回傳預設配置。 -func Defaults() Config -``` - -## Domain Models - -```go -// internal/server/models.go - -// Request -type ChatMessage struct { - Role string `json:"role"` - Content string `json:"content"` -} - -type ChatCompletionRequest struct { - Model string `json:"model"` - Messages []ChatMessage `json:"messages"` - Stream bool `json:"stream"` - Temperature *float64 `json:"temperature,omitempty"` -} - -// Response (non-streaming) -type ChatCompletionResponse struct { - ID string `json:"id"` - Object string `json:"object"` // "chat.completion" - Choices []Choice `json:"choices"` - Usage Usage `json:"usage"` -} - -type Choice struct { - Index int `json:"index"` - Message ChatMessage `json:"message"` - FinishReason string `json:"finish_reason"` -} - -type Usage struct { - PromptTokens int `json:"prompt_tokens"` - CompletionTokens int `json:"completion_tokens"` - TotalTokens int `json:"total_tokens"` -} - -// Streaming chunk -type ChatCompletionChunk struct { - ID string `json:"id"` - Object string `json:"object"` // "chat.completion.chunk" - Choices []ChunkChoice `json:"choices"` -} - -type ChunkChoice struct { - Index int `json:"index"` - Delta Delta `json:"delta"` - FinishReason string `json:"finish_reason,omitempty"` -} - -type Delta struct { - Role *string `json:"role,omitempty"` - Content *string `json:"content,omitempty"` -} - -// Models list -type ModelList struct { - Object string `json:"object"` // "list" - Data []ModelInfo `json:"data"` -} - -type ModelInfo struct { - ID string `json:"id"` - Object string `json:"object"` // "model" - Created int64 `json:"created"` - OwnedBy string `json:"owned_by"` -} - -// Error response -type ErrorResponse struct { - Error ErrorBody `json:"error"` -} - -type ErrorBody struct { - Message string `json:"message"` - Type string `json:"type"` - Code string `json:"code,omitempty"` -} -``` - -## Database Implementation Design - -N/A。無資料庫。 - -## Error Design - -```go -// internal/server/handler.go 中定義 sentinel errors + 錯誤回傳邏輯 - -var ( - ErrInvalidRequest = errors.New("invalid_request") - ErrModelNotFound = errors.New("model_not_found") - ErrCLITimeout = errors.New("cli_timeout") - ErrCLICrash = errors.New("cli_crash") - ErrCLINotAvailable = errors.New("cli_not_available") -) - -// writeError 將 error 轉換為 JSON error response 並回傳。 -func writeError(w http.ResponseWriter, err error) { - var status int - var errType string - - switch { - case errors.Is(err, ErrInvalidRequest): - status = http.StatusBadRequest - errType = "invalid_request" - case errors.Is(err, ErrModelNotFound): - status = http.StatusNotFound - errType = "model_not_found" - case errors.Is(err, ErrCLITimeout): - status = http.StatusGatewayTimeout - errType = "timeout" - default: - status = http.StatusInternalServerError - errType = "internal_error" - } - - // 回傳 ErrorResponse JSON -} -``` - -### Error-to-HTTP Mapping - -| Sentinel Error | HTTP Status | Error Type | -|---------------|-------------|------------| -| ErrInvalidRequest | 400 | invalid_request | -| ErrModelNotFound | 404 | model_not_found | -| ErrCLITimeout | 504 | timeout | -| ErrCLICrash | 500 | internal_error | -| ErrCLINotAvailable | 500 | internal_error | - -## Dependency Injection - -手動 wiring 在 `main.go`,使用 constructor injection: - -```go -// main.go -func run(cmd *cobra.Command, args []string) error { - // 1. Load config - cfg, err := config.Load(configPath) - if err != nil { - return fmt.Errorf("load config: %w", err) - } - - // 2. Create bridge - br := bridge.NewCLIBridge( - cfg.CursorCLIPath, - cfg.MaxConcurrent, - time.Duration(cfg.Timeout)*time.Second, - ) - - // 3. Check CLI availability - if err := br.CheckHealth(context.Background()); err != nil { - return fmt.Errorf("cursor cli not available: %w", err) - } - - // 4. Create and run server - srv := server.New(cfg, br) - return srv.Run() -} -``` - -## Configuration - -```yaml -# config.example.yaml -port: 8976 -cursor_cli_path: agent -default_model: claude-sonnet-4-20250514 -timeout: 300 -max_concurrent: 5 -log_level: INFO - -# optional: 手動指定可用模型 -available_models: - - claude-sonnet-4-20250514 - - claude-opus-4-20250514 - - gpt-5.2 - - gemini-3.1-pro -``` - -Config 載入順序:defaults → YAML → CLI flags。 - -## Testing Architecture - -### Unit Tests(table-driven) - -```go -// internal/converter/convert_test.go -func TestToOpenAIChunk(t *testing.T) { - tests := []struct { - name string - input string - expected ChatCompletionChunk - }{ - {"assistant line", `{"type":"assistant","content":"Hello"}`, ...}, - {"result line", `{"type":"result","content":"..."}`, ...}, - {"empty line", "", ...}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // ... - }) - } -} - -// internal/config/config_test.go -func TestLoad(t *testing.T) { ... } -func TestDefaults(t *testing.T) { ... } - -// internal/server/handler_test.go(使用 httptest) -func TestHealthEndpoint(t *testing.T) { ... } -func TestChatCompletionInvalid(t *testing.T) { ... } -``` - -### Mock Strategy - -```go -// internal/bridge/mock_test.go -type MockBridge struct { - OutputLines []string // 預設回傳的 stdout lines - Err error -} - -func (m *MockBridge) Execute(ctx context.Context, prompt, model string) (<-chan string, <-chan error) { - outCh := make(chan string) - errCh := make(chan error, 1) - go func() { - defer close(outCh) - defer close(errCh) - for _, line := range m.OutputLines { - outCh <- line - } - if m.Err != nil { - errCh <- m.Err - } - }() - return outCh, errCh -} -``` - -### Integration Tests - -```go -// internal/bridge/bridge_test.go(需要實際 Cursor CLI 環境) -func TestExecuteSimple(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - // 實際 spawn agent -} -``` - -## Build & Deployment - -### Makefile - -```makefile -.PHONY: build run test lint fmt - -build: - go build -o bin/cursor-adapter . - -run: build - ./bin/cursor-adapter --port 8976 - -test: - go test ./... -v -short - -test-integration: - go test ./... -v - -lint: - golangci-lint run - -fmt: - gofmt -w . - goimports -w . - -cross: - GOOS=darwin GOARCH=arm64 go build -o bin/cursor-adapter-darwin-arm64 . - GOOS=darwin GOARCH=amd64 go build -o bin/cursor-adapter-darwin-amd64 . - GOOS=linux GOARCH=amd64 go build -o bin/cursor-adapter-linux-amd64 . -``` - -### go.mod - -``` -module github.com/daniel/cursor-adapter - -go 1.22 - -require ( - github.com/go-chi/chi/v5 v5.x.x - github.com/spf13/cobra v1.x.x - gopkg.in/yaml.v3 v3.x.x -) -``` - -## Architecture Traceability - -| Architecture Element | Code Design Element | -|---------------------|-------------------| -| HTTP Server (chi) | internal/server/server.go + handler.go | -| CLI Bridge | internal/bridge/bridge.go — CLIBridge | -| Stream Converter | internal/converter/convert.go | -| Config Module | internal/config/config.go | -| Error Handler | internal/server/handler.go — writeError() | -| Request/Response | internal/server/models.go | -| SSE | internal/server/sse.go | - -## Code Design Review - -N/A(首次設計)。 - -## Open Questions - -1. Cursor CLI stream-json 的確切 JSON schema?(需 scripts/test_cursor_cli.sh 確認) -2. 是否需要 `-ldflags` 減小 binary? -3. 是否需要 Goreleaser 做 release? diff --git a/docs/cursor-cli-format.md b/docs/cursor-cli-format.md deleted file mode 100644 index f2e9aba..0000000 --- a/docs/cursor-cli-format.md +++ /dev/null @@ -1,47 +0,0 @@ -# Cursor CLI stream-json 格式 - -## 實際輸出格式(已確認) - -NDJSON(每行一個 JSON)。 - -### 1. System Init -```json -{"type":"system","subtype":"init","apiKeySource":"login","cwd":"/path","session_id":"uuid","model":"Auto","permissionMode":"default"} -``` - -### 2. User Message -```json -{"type":"user","message":{"role":"user","content":[{"type":"text","text":"prompt text"}]},"session_id":"uuid"} -``` - -### 3. Assistant Message(可能多次出現) -```json -{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"response text"}]},"session_id":"uuid","timestamp_ms":1776157308323} -``` - -### 4. Result(最後一行) -```json -{"type":"result","subtype":"success","duration_ms":10208,"duration_api_ms":10208,"is_error":false,"result":"OK","session_id":"uuid","request_id":"uuid","usage":{"inputTokens":0,"outputTokens":122,"cacheReadTokens":5120,"cacheWriteTokens":14063}} -``` - -## 轉換規則 - -| Cursor type | 行為 | -|-------------|------| -| system | 忽略(初始化訊息)| -| user | 忽略(echo 回用戶訊息)| -| assistant | 提取 message.content[].text → OpenAI delta.content | -| result (success) | 提取 usage → OpenAI usage,發送 finish_reason:"stop" | -| result (error) | 發送錯誤 chunk | - -## CLI 參數 - -```bash -agent -p "prompt" \ - --output-format stream-json \ - --stream-partial-output \ - --trust \ - --model "model-name" -``` - -注意:需要 `--trust` 才能在非互動模式執行。 diff --git a/docs/plan/2026-04-14-cursor-adapter.md b/docs/plan/2026-04-14-cursor-adapter.md deleted file mode 100644 index d34e797..0000000 --- a/docs/plan/2026-04-14-cursor-adapter.md +++ /dev/null @@ -1,215 +0,0 @@ -# Plan: Cursor Adapter - -## Overview - -用 Go 實作一個本機 OpenAI-compatible proxy,透過 spawn Cursor CLI 的 headless 模式來使用 Cursor 的模型。 - -## Inputs - -- Architecture: `docs/architecture/2026-04-14-cursor-adapter.md` -- Code Design: `docs/code-design/2026-04-14-cursor-adapter.md` - -## Planning Assumptions - -- Go 1.22+ -- 使用 chi v5 做 HTTP router -- 使用 cobra 做 CLI -- 專案目錄:`~/Documents/projects/cursor-adapter/` -- 先探索 Cursor CLI 輸出格式,再實作轉換邏輯 - -## Task Breakdown - -### Task P1: Go Module 初始化 + 專案結構 -- Objective: `go mod init`、建立目錄結構、空檔案 -- Inputs Used: Code Design — Project Structure -- Design References: code-design §Project Structure -- Dependencies: None -- Deliverables: go.mod、目錄結構、空的 package files -- Completion Criteria: `go build` 成功(即使什麼都沒做) - -### Task P2: 探索 Cursor CLI 輸出格式 -- Objective: 實際跑 `agent -p "hello" --output-format stream-json`,記錄每行 JSON 的結構 -- Inputs Used: PRD — Open Question 1 -- Design References: architecture §Open Questions -- Dependencies: P1 -- Deliverables: scripts/test_cursor_cli.sh、記錄 Cursor stream-json schema 的文件 -- Completion Criteria: 明確知道每行 JSON 的 type/content 結構 - -### Task P3: Config 模組 -- Objective: internal/config/config.go(YAML 載入、Defaults、驗證) -- Inputs Used: Code Design — Configuration -- Design References: code-design §Config struct, §Load() -- Dependencies: P1 -- Deliverables: internal/config/config.go、internal/config/config_test.go、config.example.yaml -- Completion Criteria: table-driven test 通過 - -### Task P4: Models(Request/Response Structs) -- Objective: internal/server/models.go(所有 JSON struct + JSON tags) -- Inputs Used: Code Design — Domain Models -- Design References: code-design §Domain Models -- Dependencies: P1 -- Deliverables: internal/server/models.go -- Completion Criteria: struct 定義完成,JSON tag 正確 - -### Task P5: SSE Helper -- Objective: internal/server/sse.go(SSE 格式化、flush helpers) -- Inputs Used: Architecture — SSE streaming -- Design References: architecture §API Contract -- Dependencies: P4 -- Deliverables: internal/server/sse.go -- Completion Criteria: FormatSSE() / FormatDone() 正確 - -### Task P6: CLI Bridge -- Objective: internal/bridge/bridge.go(Bridge interface + CLIBridge 實作) -- Inputs Used: Code Design — Interface Definitions -- Design References: code-design §Bridge interface, §CLIBridge -- Dependencies: P2, P3 -- Deliverables: internal/bridge/bridge.go、internal/bridge/bridge_test.go -- Completion Criteria: Execute() 能 spawn agent、逐行 yield、timeout kill - -### Task P7: Stream Converter -- Objective: internal/converter/convert.go(Cursor JSON → OpenAI SSE 轉換) -- Inputs Used: Code Design — Converter interface -- Design References: code-design §ToOpenAIChunk(), §ToOpenAIResponse() -- Dependencies: P2, P4 -- Deliverables: internal/converter/convert.go、internal/converter/convert_test.go -- Completion Criteria: table-driven test 通過,轉換格式正確 - -### Task P8: HTTP Server + Handlers -- Objective: internal/server/server.go + handler.go(chi router、3 個 endpoint、error middleware) -- Inputs Used: Code Design — Layer Architecture -- Design References: code-design §handler.go, §writeError() -- Dependencies: P3, P4, P5, P6, P7 -- Deliverables: internal/server/server.go、internal/server/handler.go、internal/server/handler_test.go -- Completion Criteria: /health、/v1/models、/v1/chat/completions 可回應(用 mock bridge 測試) - -### Task P9: CLI 入口(main.go) -- Objective: main.go(cobra command、wiring、啟動 server) -- Inputs Used: Code Design — Dependency Injection -- Design References: code-design §main.go wiring -- Dependencies: P3, P8 -- Deliverables: main.go -- Completion Criteria: `go build && ./cursor-adapter` 啟動成功 - -### Task P10: 整合測試 -- Objective: 實際用 curl 和 Hermes 測試完整流程 -- Inputs Used: PRD — Acceptance Criteria -- Design References: architecture §API Contract -- Dependencies: P9 -- Deliverables: 測試結果記錄 -- Completion Criteria: AC1-AC5 通過 - -### Task P11: README -- Objective: 安裝、設定、使用方式 -- Inputs Used: PRD, Architecture -- Dependencies: P10 -- Deliverables: README.md -- Completion Criteria: 新使用者看著 README 能跑起來 - -## Dependency Graph - -```mermaid -graph TD - P1 --> P2 - P1 --> P3 - P1 --> P4 - P2 --> P6 - P2 --> P7 - P3 --> P6 - P3 --> P9 - P4 --> P5 - P4 --> P7 - P4 --> P8 - P5 --> P8 - P6 --> P8 - P7 --> P8 - P8 --> P9 - P9 --> P10 - P10 --> P11 -``` - -## Execution Order - -### Phase 1: Foundation(可並行) -- P1 Go Module 初始化 -- P3 Config 模組 -- P4 Models - -### Phase 2: Exploration(需 P1) -- P2 探索 Cursor CLI 輸出格式 -- P5 SSE Helper(需 P4) - -### Phase 3: Core Logic(需 P2 + Phase 1) -- P6 CLI Bridge -- P7 Stream Converter - -### Phase 4: Integration(需 Phase 3) -- P8 HTTP Server + Handlers -- P9 CLI 入口 - -### Phase 5: Validation(需 P9) -- P10 整合測試 -- P11 README - -## Milestones - -### Milestone M1: Foundation Ready -- Included Tasks: P1, P3, P4 -- Exit Criteria: go.mod 存在、config 可載入、models struct 定義完成 - -### Milestone M2: Core Logic Complete -- Included Tasks: P2, P5, P6, P7 -- Exit Criteria: CLI Bridge 能 spawn Cursor、converter 轉換正確 - -### Milestone M3: MVP Ready -- Included Tasks: P8, P9, P10, P11 -- Exit Criteria: `cursor-adapter` 啟動後,curl AC1-AC5 通過 - -## Deliverables - -| Task | Deliverable | Source Design Reference | -|------|-------------|------------------------| -| P1 | go.mod + project structure | code-design §Project Structure | -| P2 | test script + format docs | architecture §Open Questions | -| P3 | internal/config/config.go + test | code-design §Configuration | -| P4 | internal/server/models.go | code-design §Domain Models | -| P5 | internal/server/sse.go | architecture §SSE | -| P6 | internal/bridge/bridge.go + test | code-design §Bridge interface | -| P7 | internal/converter/convert.go + test | code-design §Converter | -| P8 | server.go + handler.go + test | code-design §Layer Architecture | -| P9 | main.go | code-design §Dependency Injection | -| P10 | test results | PRD §Acceptance Criteria | -| P11 | README.md | — | - -## Design Traceability - -| Upstream Design Element | Planned Task(s) | -|-------------------------|-----------------| -| Architecture: HTTP Server | P8 | -| Architecture: CLI Bridge | P6 | -| Architecture: Stream Converter | P7 | -| Architecture: SSE | P5 | -| Architecture: Config | P3 | -| Architecture: Error Model | P8 | -| Code Design: Project Structure | P1 | -| Code Design: Interface — Bridge | P6 | -| Code Design: Interface — Converter | P7 | -| Code Design: Domain Models | P4 | -| Code Design: Configuration | P3 | -| Code Design: DI — main.go | P9 | - -## Risks And Sequencing Notes - -- P2(探索 Cursor CLI 格式)是 critical path — P6 和 P7 依賴此結果 -- P6(CLI Bridge)是最複雜的任務 — goroutine、subprocess、timeout、semaphore -- 如果 Cursor CLI stream-json 格式複雜,P7 可能需要迭代 -- P10 可能發現 edge case,需要回頭修 P6/P7/P8 - -## Planning Review - -N/A(首次規劃)。 - -## Open Questions - -1. Cursor CLI stream-json 的確切格式?(P2 回答) -2. Cursor CLI 並發限制?(P10 確認) diff --git a/docs/prd/2026-04-14-cursor-adapter.md b/docs/prd/2026-04-14-cursor-adapter.md deleted file mode 100644 index 47db683..0000000 --- a/docs/prd/2026-04-14-cursor-adapter.md +++ /dev/null @@ -1,163 +0,0 @@ -## Research Inputs - -N/A。這是個人工具,不需要市場研究。 - -## Problem - -我同時使用多個 CLI AI 工具(Hermes Agent、OpenCode、Claude Code),這些工具都支援自訂 API base URL 和 model。我的公司有 Cursor 帳號,透過 `agent login` 已在本機完成認證,可以使用 Cursor 提供的多種模型。 - -目前的問題是:每支 CLI 工具都需要自己買 API key 或設定 provider,但我已經有 Cursor 帳號的額度可以用。我需要一個轉接器,讓這些 CLI 工具能透過 Cursor CLI 的 headless 模式來使用 Cursor 的模型,省去額外的 API 費用。 - -## Goals - -- 本機跑一個 HTTP server,提供 OpenAI-compatible API(`/v1/chat/completions`) -- 收到請求後,spawn Cursor CLI 的 `agent` 子程序來執行 -- 將 Cursor CLI 的 streaming JSON 輸出轉換成 OpenAI SSE 格式回傳 -- 支援多種 Cursor 模型的選擇 -- 零額外認證設定 — 直接使用本機已有的 Cursor 登入狀態 - -## Non Goals - -- 不支援非 OpenAI format 的 CLI 工具 -- 不做 API key 管理或多用戶認證 -- 不做計量、追蹤、計費功能 -- 不做模型負載平衡或 failover -- 不代理 Cursor IDE 的功能,只代理 headless CLI 模式 - -## Scope - -本機 personal proxy server,一個使用者,本機部署。 - -### In Scope - -- OpenAI-compatible API(`/v1/chat/completions`、`/v1/models`) -- SSE streaming response -- 模型選擇(透過 `--model` 參數傳給 Cursor CLI) -- 簡單的 YAML config 檔設定 -- 錯誤處理和 CLI 子程序生命週期管理 -- health check endpoint - -### Out of Scope - -- 非 OpenAI format 支援 -- 多用戶 / API key 管理 -- 計量追蹤 -- GUI 介面 -- Docker 部署 - -## Success Metrics - -- Hermes Agent、OpenCode、Claude Code 都能透過設定 base URL 指向此 proxy 來使用 Cursor 模型 -- streaming 回應的延遲 < 2 秒(不含模型思考時間) -- proxy 啟動後零設定即可使用(只需改 CLI 工具的 config) - -## User Stories - -1. 作為使用者,我想啟動 proxy server,這樣我的 CLI 工具就能連到它 -2. 作為使用者,我想在 Hermes Agent 裡設定 `base_url = http://localhost:8976`,這樣就能用 Cursor 的模型 -3. 作為使用者,我想在 CLI 工具裡指定 `model = claude-sonnet-4-20250514`,proxy 會傳給 Cursor CLI -4. 作為使用者,我想看到模型的思考過程即時串流到終端機上 -5. 作為使用者,我想透過 `/v1/models` 查看可用的模型列表 -6. 作為使用者,我想透過 config 檔設定 proxy 的 port 和其他選項 - -## Functional Requirements - -### FR1: OpenAI-Compatible API -- 支援 `POST /v1/chat/completions` -- 接受 OpenAI 格式的 request body(`model`、`messages`、`stream`) -- 當 `stream: true` 時,回傳 SSE 格式的 `data: {...}\n\n` chunks -- 當 `stream: false` 時,回傳完整的 JSON response - -### FR2: Cursor CLI Integration -- 收到請求後,組合 prompt 從 messages 陣列 -- spawn `agent -p "{prompt}" --model "{model}" --output-format stream-json` 子程序 -- 讀取子程序的 stdout streaming JSON 輸出 -- 管理子程序生命週期(啟動、執行、結束、超時 kill) - -### FR3: Streaming Response Conversion -- 將 Cursor CLI 的 `stream-json` 輸出轉換成 OpenAI SSE 格式 -- 每個 SSE chunk 需包含 `id`、`object: "chat.completion.chunk"`、`choices[0].delta.content` -- 最後一個 chunk 需包含 `finish_reason: "stop"` - -### FR4: Model Listing -- 支援 `GET /v1/models`,回傳可用模型列表 -- 模型列表從 Cursor CLI 取得(`agent --list-models` 或 config 定義) - -### FR5: Configuration -- YAML config 檔(預設 `~/.cursor-adapter/config.yaml`) -- 可設定:port、cursor_cli_path、default_model、timeout - -### FR6: Error Handling -- Cursor CLI 超時(可設定,預設 5 分鐘)→ 回傳 504 -- Cursor CLI 錯誤 → 回傳 500 + 錯誤訊息 -- 無效的 request body → 回傳 400 -- model 不存在 → 回傳 404 - -## Acceptance Criteria - -### AC1: Basic Chat Completion -Given proxy 已啟動在 port 8976,When 我用 curl 發送 `POST /v1/chat/completions` 帶上 `{"model": "claude-sonnet-4-20250514", "messages": [{"role": "user", "content": "hello"}], "stream": true}`,Then 收到 SSE streaming response,且內容為 Cursor CLI 的回應轉換成的 OpenAI 格式。 - -### AC2: Streaming Display -Given CLI 工具連到 proxy 並發送 streaming 請求,When 模型正在生成回應,Then CLI 工具的終端機上即時顯示文字內容(不需要等完整回應)。 - -### AC3: Model Selection -Given proxy 已啟動,When 請求中指定 `model: "gpt-5.2"`,Then proxy spawn Cursor CLI 時使用 `--model gpt-5.2`。 - -### AC4: Health Check -Given proxy 已啟動,When 發送 `GET /health`,Then 回傳 `{"status": "ok", "cursor_cli": "available"}`。 - -### AC5: Model Listing -Given proxy 已啟動,When 發送 `GET /v1/models`,Then 回傳 Cursor 可用的模型列表,格式符合 OpenAI models API。 - -## Edge Cases - -- Cursor CLI 子程序意外崩潰 → proxy 回傳 500,清理資源 -- 請求 timeout(模型思考太久)→ proxy kill 子程序,回傳 504 -- 並發請求 → 每個請求 spawn 獨立的子程序 -- Cursor CLI 未安裝或不在 PATH → proxy 啟動時檢查,啟動失敗時給明確錯誤 -- Cursor CLI 未登入 → proxy 回傳錯誤訊息提示先 `agent login` -- messages 陣列為空 → 回傳 400 -- stream: false 時,需要等 Cursor CLI 完整輸出後才回傳 - -## Non Functional Requirements - -### NFR1: Performance -- proxy 自身的 overhead < 500ms(不含模型思考時間) -- streaming 的第一個 token 延遲不超過 Cursor CLI 本身的延遲 + 200ms - -### NFR2: Reliability -- 並發請求數 ≤ 5(個人使用) -- 子程序超時後正確清理,不留 zombie process - -### NFR3: Usability -- 一行命令啟動:`cursor-adapter` 或 `cursor-adapter --port 8976` -- config 檔格式簡單,有合理的預設值 -- 啟動時顯示可用模型列表 - -## Risks - -| Risk | Impact | Likelihood | Mitigation | -|------|--------|-----------|------------| -| Cursor CLI output format 變更 | High | Medium | 抽象輸出解析層,方便適配 | -| Cursor CLI 不支援某些模型 | Medium | Low | 啟動時驗證模型可用性 | -| 並發子程序過多導致資源耗盡 | Medium | Low | 限制最大並發數 | -| Cursor 的 headless 模式有限制 | High | Medium | 先用 headless 模式測試,必要時 fallback 到 ACP | - -## Assumptions - -- Cursor CLI 已安裝且在 PATH 中 -- Cursor CLI 已透過 `agent login` 完成認證 -- 使用者的 CLI 工具都支援 OpenAI-compatible API format -- 使用者只需要 `/v1/chat/completions` 和 `/v1/models` 兩個 endpoint - -## Dependencies - -- Cursor CLI(`agent` command) -- Python 3.10+ 或 Node.js 18+(取決於實作語言選擇) - -## Open Questions - -1. Cursor CLI 的 `--output-format stream-json` 的確切 JSON schema 是什麼?需要實際跑一次來確認 -2. Cursor CLI 是否支援同時跑多個 headless 實例? -3. 需要支援 function calling / tool use 嗎?(目前 PRD 不含,但如果 Cursor CLI 支援的話可以加) diff --git a/docs/test-output-log.md b/docs/test-output-log.md deleted file mode 100644 index 518d3e6..0000000 --- a/docs/test-output-log.md +++ /dev/null @@ -1,37 +0,0 @@ -# Cursor CLI Test Output -# Date: $(date) -# Script: scripts/test_cursor_cli.sh - -## Test 1: agent --version -``` -2026.04.13-a9d7fb5 -``` - -## Test 2: agent -p "say hello in one word" --output-format stream-json --trust -- 需要 --trust 參數在非互動模式執行 -- --force/-f 被團隊管理員禁用 -- 執行後等待回應中 (可能需要較長時間初始化) - -## Test 3: agent --help (output-format 相關) -``` ---output-format Output format (only works with --print): text | - json | stream-json (default: "text") ---stream-partial-output Stream partial output as individual text deltas - (only works with --print and stream-json format) - (default: false) -``` - -## Test 4: agent models (部分結果) -- auto (預設) -- composer-2-fast, composer-2, composer-1.5 -- gpt-5.3-codex 系列 (low/normal/high/xhigh) -- claude-4-sonnet, claude-4.5-sonnet -- grok-4-20 -- gemini-3-flash -- kimi-k2.5 - -## 結論 -1. Cursor CLI 已安裝且可用 -2. stream-json 需要配合 --print (-p) 和 --trust 使用 -3. 有 --stream-partial-output 可取得逐字串流 -4. 實際的 JSON 格式需要等待回應完成後才能解析