diff --git a/pkg/usecase/cmdargs.go b/pkg/usecase/cmdargs.go
new file mode 100644
index 0000000..d5e8f6e
--- /dev/null
+++ b/pkg/usecase/cmdargs.go
@@ -0,0 +1,28 @@
+package usecase
+
+import "cursor-api-proxy/internal/config"
+
+func BuildAgentFixedArgs(cfg config.BridgeConfig, workspaceDir, model string, stream bool) []string {
+ args := []string{"--print"}
+ if cfg.ApproveMcps {
+ args = append(args, "--approve-mcps")
+ }
+ if cfg.Force {
+ args = append(args, "--force")
+ }
+ if cfg.ChatOnlyWorkspace {
+ args = append(args, "--trust")
+ }
+ args = append(args, "--workspace", workspaceDir)
+ args = append(args, "--model", model)
+ if stream {
+ args = append(args, "--stream-partial-output", "--output-format", "stream-json")
+ } else {
+ args = append(args, "--output-format", "text")
+ }
+ return args
+}
+
+func BuildAgentCmdArgs(cfg config.BridgeConfig, workspaceDir, model, prompt string, stream bool) []string {
+ return append(BuildAgentFixedArgs(cfg, workspaceDir, model, stream), prompt)
+}
diff --git a/pkg/usecase/maxmode.go b/pkg/usecase/maxmode.go
new file mode 100644
index 0000000..291113b
--- /dev/null
+++ b/pkg/usecase/maxmode.go
@@ -0,0 +1,85 @@
+package usecase
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "runtime"
+)
+
+func getCandidates(agentScriptPath, configDirOverride string) []string {
+ if configDirOverride != "" {
+ return []string{filepath.Join(configDirOverride, "cli-config.json")}
+ }
+
+ var result []string
+
+ if dir := os.Getenv("CURSOR_CONFIG_DIR"); dir != "" {
+ result = append(result, filepath.Join(dir, "cli-config.json"))
+ }
+
+ if agentScriptPath != "" {
+ agentDir := filepath.Dir(agentScriptPath)
+ result = append(result, filepath.Join(agentDir, "..", "data", "config", "cli-config.json"))
+ }
+
+ home := os.Getenv("HOME")
+ if home == "" {
+ home = os.Getenv("USERPROFILE")
+ }
+
+ switch runtime.GOOS {
+ case "windows":
+ local := os.Getenv("LOCALAPPDATA")
+ if local == "" {
+ local = filepath.Join(home, "AppData", "Local")
+ }
+ result = append(result, filepath.Join(local, "cursor-agent", "cli-config.json"))
+ case "darwin":
+ result = append(result, filepath.Join(home, "Library", "Application Support", "cursor-agent", "cli-config.json"))
+ default:
+ xdg := os.Getenv("XDG_CONFIG_HOME")
+ if xdg == "" {
+ xdg = filepath.Join(home, ".config")
+ }
+ result = append(result, filepath.Join(xdg, "cursor-agent", "cli-config.json"))
+ }
+
+ return result
+}
+
+func RunMaxModePreflight(agentScriptPath, configDirOverride string) {
+ for _, candidate := range getCandidates(agentScriptPath, configDirOverride) {
+ data, err := os.ReadFile(candidate)
+ if err != nil {
+ continue
+ }
+
+ // Strip BOM if present
+ if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
+ data = data[3:]
+ }
+
+ var raw map[string]interface{}
+ if err := json.Unmarshal(data, &raw); err != nil {
+ continue
+ }
+ if raw == nil || len(raw) <= 1 {
+ continue
+ }
+
+ raw["maxMode"] = true
+ if model, ok := raw["model"].(map[string]interface{}); ok {
+ model["maxMode"] = true
+ }
+
+ out, err := json.MarshalIndent(raw, "", " ")
+ if err != nil {
+ continue
+ }
+ if err := os.WriteFile(candidate, out, 0644); err != nil {
+ continue
+ }
+ return
+ }
+}
diff --git a/pkg/usecase/runner.go b/pkg/usecase/runner.go
new file mode 100644
index 0000000..ee4b093
--- /dev/null
+++ b/pkg/usecase/runner.go
@@ -0,0 +1,72 @@
+package usecase
+
+import (
+ "context"
+ "cursor-api-proxy/internal/config"
+ "cursor-api-proxy/pkg/infrastructure/process"
+ "os"
+ "path/filepath"
+)
+
+func init() {
+ process.MaxModeFn = RunMaxModePreflight
+}
+
+func cacheTokenForAccount(configDir string) {
+ if configDir == "" {
+ return
+ }
+ token := ReadKeychainToken()
+ if token != "" {
+ WriteCachedToken(configDir, token)
+ }
+}
+
+func AccountsDir() string {
+ home := os.Getenv("HOME")
+ if home == "" {
+ home = os.Getenv("USERPROFILE")
+ }
+ return filepath.Join(home, ".cursor-api-proxy", "accounts")
+}
+
+func RunAgentSync(cfg config.BridgeConfig, workspaceDir string, cmdArgs []string, tempDir, configDir string, ctx context.Context) (process.RunResult, error) {
+ opts := process.RunOptions{
+ Cwd: workspaceDir,
+ TimeoutMs: cfg.TimeoutMs,
+ MaxMode: cfg.MaxMode,
+ ConfigDir: configDir,
+ Ctx: ctx,
+ }
+
+ result, err := process.Run(cfg.AgentBin, cmdArgs, opts)
+
+ cacheTokenForAccount(configDir)
+ if tempDir != "" {
+ os.RemoveAll(tempDir)
+ }
+
+ return result, err
+}
+
+func RunAgentStreamWithContext(cfg config.BridgeConfig, workspaceDir string, cmdArgs []string, onLine func(string), tempDir, configDir string, ctx context.Context) (process.StreamResult, error) {
+ opts := process.RunStreamingOptions{
+ RunOptions: process.RunOptions{
+ Cwd: workspaceDir,
+ TimeoutMs: cfg.TimeoutMs,
+ MaxMode: cfg.MaxMode,
+ ConfigDir: configDir,
+ Ctx: ctx,
+ },
+ OnLine: onLine,
+ }
+
+ result, err := process.RunStreaming(cfg.AgentBin, cmdArgs, opts)
+
+ cacheTokenForAccount(configDir)
+ if tempDir != "" {
+ os.RemoveAll(tempDir)
+ }
+
+ return result, err
+}
diff --git a/pkg/usecase/sanitizer.go b/pkg/usecase/sanitizer.go
new file mode 100644
index 0000000..661df80
--- /dev/null
+++ b/pkg/usecase/sanitizer.go
@@ -0,0 +1,95 @@
+package usecase
+
+import "regexp"
+
+type rule struct {
+ pattern *regexp.Regexp
+ replacement string
+}
+
+var rules = []rule{
+ {regexp.MustCompile(`(?i)x-anthropic-billing-header:[^\n]*\n?`), ""},
+ {regexp.MustCompile(`(?i)\bcc_version=[^\s;,\n]+[;,]?\s*`), ""},
+ {regexp.MustCompile(`(?i)\bcc_entrypoint=[^\s;,\n]+[;,]?\s*`), ""},
+ {regexp.MustCompile(`(?i)\bcch=[a-f0-9]+[;,]?\s*`), ""},
+ {regexp.MustCompile(`\bClaude Code\b`), "Cursor"},
+ {regexp.MustCompile(`(?i)Anthropic['']s official CLI for Claude`), "Cursor AI assistant"},
+ {regexp.MustCompile(`\bAnthropic\b`), "Cursor"},
+ {regexp.MustCompile(`(?i)anthropic\.com`), "cursor.com"},
+ {regexp.MustCompile(`(?i)claude\.ai`), "cursor.sh"},
+ {regexp.MustCompile(`^[;,\s]+`), ""},
+}
+
+func SanitizeText(text string) string {
+ for _, r := range rules {
+ text = r.pattern.ReplaceAllString(text, r.replacement)
+ }
+ return text
+}
+
+func SanitizeMessages(messages []interface{}) []interface{} {
+ result := make([]interface{}, len(messages))
+ for i, raw := range messages {
+ if raw == nil {
+ result[i] = raw
+ continue
+ }
+ m, ok := raw.(map[string]interface{})
+ if !ok {
+ result[i] = raw
+ continue
+ }
+ newMsg := make(map[string]interface{}, len(m))
+ for k, v := range m {
+ newMsg[k] = v
+ }
+ switch c := m["content"].(type) {
+ case string:
+ newMsg["content"] = SanitizeText(c)
+ case []interface{}:
+ newParts := make([]interface{}, len(c))
+ for j, p := range c {
+ if pm, ok := p.(map[string]interface{}); ok && pm["type"] == "text" {
+ if t, ok := pm["text"].(string); ok {
+ newPart := make(map[string]interface{}, len(pm))
+ for k, v := range pm {
+ newPart[k] = v
+ }
+ newPart["text"] = SanitizeText(t)
+ newParts[j] = newPart
+ continue
+ }
+ }
+ newParts[j] = p
+ }
+ newMsg["content"] = newParts
+ }
+ result[i] = newMsg
+ }
+ return result
+}
+
+func SanitizeSystem(system interface{}) interface{} {
+ switch v := system.(type) {
+ case string:
+ return SanitizeText(v)
+ case []interface{}:
+ result := make([]interface{}, len(v))
+ for i, p := range v {
+ if pm, ok := p.(map[string]interface{}); ok && pm["type"] == "text" {
+ if t, ok := pm["text"].(string); ok {
+ newPart := make(map[string]interface{}, len(pm))
+ for k, val := range pm {
+ newPart[k] = val
+ }
+ newPart["text"] = SanitizeText(t)
+ result[i] = newPart
+ continue
+ }
+ }
+ result[i] = p
+ }
+ return result
+ }
+ return system
+}
diff --git a/pkg/usecase/sanitizer_test.go b/pkg/usecase/sanitizer_test.go
new file mode 100644
index 0000000..393b510
--- /dev/null
+++ b/pkg/usecase/sanitizer_test.go
@@ -0,0 +1,60 @@
+package usecase
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestSanitizeTextAnthropicBilling(t *testing.T) {
+ input := "x-anthropic-billing-header: abc123\nHello"
+ got := SanitizeText(input)
+ if strings.Contains(got, "x-anthropic-billing-header") {
+ t.Errorf("billing header not removed: %q", got)
+ }
+}
+
+func TestSanitizeTextClaudeCode(t *testing.T) {
+ input := "I am Claude Code assistant"
+ got := SanitizeText(input)
+ if strings.Contains(got, "Claude Code") {
+ t.Errorf("'Claude Code' not replaced: %q", got)
+ }
+ if !strings.Contains(got, "Cursor") {
+ t.Errorf("'Cursor' not present in output: %q", got)
+ }
+}
+
+func TestSanitizeTextAnthropic(t *testing.T) {
+ input := "Powered by Anthropic's technology and anthropic.com"
+ got := SanitizeText(input)
+ if strings.Contains(got, "Anthropic") {
+ t.Errorf("'Anthropic' not replaced: %q", got)
+ }
+ if strings.Contains(got, "anthropic.com") {
+ t.Errorf("'anthropic.com' not replaced: %q", got)
+ }
+}
+
+func TestSanitizeTextNoChange(t *testing.T) {
+ input := "Hello, this is a normal message about cursor."
+ got := SanitizeText(input)
+ if got != input {
+ t.Errorf("unexpected change: %q -> %q", input, got)
+ }
+}
+
+func TestSanitizeMessages(t *testing.T) {
+ messages := []interface{}{
+ map[string]interface{}{"role": "user", "content": "Ask Claude Code something"},
+ map[string]interface{}{"role": "system", "content": "Use Anthropic's tools"},
+ }
+ result := SanitizeMessages(messages)
+
+ for _, raw := range result {
+ m := raw.(map[string]interface{})
+ c := m["content"].(string)
+ if strings.Contains(c, "Claude Code") || strings.Contains(c, "Anthropic") {
+ t.Errorf("found unsanitized content: %q", c)
+ }
+ }
+}
diff --git a/pkg/usecase/token.go b/pkg/usecase/token.go
new file mode 100644
index 0000000..6dce4c6
--- /dev/null
+++ b/pkg/usecase/token.go
@@ -0,0 +1,36 @@
+package usecase
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+const tokenFile = ".cursor-token"
+
+func ReadCachedToken(configDir string) string {
+ p := filepath.Join(configDir, tokenFile)
+ data, err := os.ReadFile(p)
+ if err != nil {
+ return ""
+ }
+ return strings.TrimSpace(string(data))
+}
+
+func WriteCachedToken(configDir, token string) {
+ p := filepath.Join(configDir, tokenFile)
+ _ = os.WriteFile(p, []byte(token), 0600)
+}
+
+func ReadKeychainToken() string {
+ if runtime.GOOS != "darwin" {
+ return ""
+ }
+ out, err := exec.Command("security", "find-generic-password", "-s", "cursor-access-token", "-w").Output()
+ if err != nil {
+ return ""
+ }
+ return strings.TrimSpace(string(out))
+}
diff --git a/pkg/usecase/toolcall.go b/pkg/usecase/toolcall.go
new file mode 100644
index 0000000..37f3677
--- /dev/null
+++ b/pkg/usecase/toolcall.go
@@ -0,0 +1,154 @@
+package usecase
+
+import (
+ "encoding/json"
+ "regexp"
+ "strings"
+)
+
+type ToolCall struct {
+ Name string
+ Arguments string // JSON string
+}
+
+type ParsedResponse struct {
+ TextContent string
+ ToolCalls []ToolCall
+}
+
+func (p *ParsedResponse) HasToolCalls() bool {
+ return len(p.ToolCalls) > 0
+}
+
+// Modified regex to handle nested JSON
+var toolCallTagRe = regexp.MustCompile(`(?s)行政法规\s*(\{(?:[^{}]|\{[^{}]*\})*\})\s*ugalakh`)
+var antmlFunctionCallsRe = regexp.MustCompile(`(?s)\s*(.*?)\s*`)
+var antmlInvokeRe = regexp.MustCompile(`(?s)\s*(.*?)\s*`)
+var antmlParamRe = regexp.MustCompile(`(?s)(.*?)`)
+
+func ExtractToolCalls(text string, toolNames map[string]bool) *ParsedResponse {
+ result := &ParsedResponse{}
+
+ if locs := toolCallTagRe.FindAllStringSubmatchIndex(text, -1); len(locs) > 0 {
+ var calls []ToolCall
+ var textParts []string
+ last := 0
+ for _, loc := range locs {
+ if loc[0] > last {
+ textParts = append(textParts, text[last:loc[0]])
+ }
+ jsonStr := text[loc[2]:loc[3]]
+ if tc := parseToolCallJSON(jsonStr, toolNames); tc != nil {
+ calls = append(calls, *tc)
+ } else {
+ textParts = append(textParts, text[loc[0]:loc[1]])
+ }
+ last = loc[1]
+ }
+ if last < len(text) {
+ textParts = append(textParts, text[last:])
+ }
+ if len(calls) > 0 {
+ result.TextContent = strings.TrimSpace(strings.Join(textParts, ""))
+ result.ToolCalls = calls
+ return result
+ }
+ }
+
+ if locs := antmlFunctionCallsRe.FindAllStringSubmatchIndex(text, -1); len(locs) > 0 {
+ var calls []ToolCall
+ var textParts []string
+ last := 0
+ for _, loc := range locs {
+ if loc[0] > last {
+ textParts = append(textParts, text[last:loc[0]])
+ }
+ block := text[loc[2]:loc[3]]
+ invokes := antmlInvokeRe.FindAllStringSubmatch(block, -1)
+ for _, inv := range invokes {
+ name := inv[1]
+ if toolNames != nil && len(toolNames) > 0 && !toolNames[name] {
+ continue
+ }
+ body := inv[2]
+ args := map[string]interface{}{}
+ params := antmlParamRe.FindAllStringSubmatch(body, -1)
+ for _, p := range params {
+ paramName := p[1]
+ paramValue := strings.TrimSpace(p[2])
+ var jsonVal interface{}
+ if err := json.Unmarshal([]byte(paramValue), &jsonVal); err == nil {
+ args[paramName] = jsonVal
+ } else {
+ args[paramName] = paramValue
+ }
+ }
+ argsJSON, _ := json.Marshal(args)
+ calls = append(calls, ToolCall{Name: name, Arguments: string(argsJSON)})
+ }
+ last = loc[1]
+ }
+ if last < len(text) {
+ textParts = append(textParts, text[last:])
+ }
+ if len(calls) > 0 {
+ result.TextContent = strings.TrimSpace(strings.Join(textParts, ""))
+ result.ToolCalls = calls
+ return result
+ }
+ }
+
+ result.TextContent = text
+ return result
+}
+
+func parseToolCallJSON(jsonStr string, toolNames map[string]bool) *ToolCall {
+ var raw map[string]interface{}
+ if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil {
+ return nil
+ }
+ name, _ := raw["name"].(string)
+ if name == "" {
+ return nil
+ }
+ if toolNames != nil && len(toolNames) > 0 && !toolNames[name] {
+ return nil
+ }
+ var argsStr string
+ switch a := raw["arguments"].(type) {
+ case string:
+ argsStr = a
+ case map[string]interface{}, []interface{}:
+ b, _ := json.Marshal(a)
+ argsStr = string(b)
+ default:
+ if p, ok := raw["parameters"]; ok {
+ b, _ := json.Marshal(p)
+ argsStr = string(b)
+ } else {
+ argsStr = "{}"
+ }
+ }
+ return &ToolCall{Name: name, Arguments: argsStr}
+}
+
+func CollectToolNames(tools []interface{}) map[string]bool {
+ names := map[string]bool{}
+ for _, t := range tools {
+ m, ok := t.(map[string]interface{})
+ if !ok {
+ continue
+ }
+ if m["type"] == "function" {
+ if fn, ok := m["function"].(map[string]interface{}); ok {
+ if name, ok := fn["name"].(string); ok {
+ names[name] = true
+ }
+ }
+ }
+ if name, ok := m["name"].(string); ok {
+ names[name] = true
+ }
+ }
+ return names
+}