opencode-cursor-agent/docs/architecture/2026-04-14-cursor-adapter.md

11 KiB
Raw Blame History

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

單一 binary4 個 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:

{
  "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):

{
  "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:

{
  "object": "list",
  "data": [
    {"id": "claude-sonnet-4-20250514", "object": "model", "created": 0, "owned_by": "cursor"}
  ]
}

GET /health

Response:

{"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。不需要 queuegoroutine + 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 toolbind 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) loggingINFO 請求/完成、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

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

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

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-compilemacOS/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 usePRD 不需要)

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 實例?