merge: refactor/usecase
This commit is contained in:
commit
e379c79bd1
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)<function_calls>\s*(.*?)\s*</function_calls>`)
|
||||
var antmlInvokeRe = regexp.MustCompile(`(?s)<invoke\s+name="([^"]+)">\s*(.*?)\s*</invoke>`)
|
||||
var antmlParamRe = regexp.MustCompile(`(?s)<parameter\s+name="([^"]+)">(.*?)</parameter>`)
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Reference in New Issue