project reborn
This commit is contained in:
parent
0241da4926
commit
dae73a2de2
Binary file not shown.
|
|
@ -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 實例?
|
||||
|
|
@ -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?
|
||||
|
|
@ -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` 才能在非互動模式執行。
|
||||
|
|
@ -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 確認)
|
||||
|
|
@ -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 支援的話可以加)
|
||||
|
|
@ -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 <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 格式需要等待回應完成後才能解析
|
||||
Loading…
Reference in New Issue