# 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 實例？
