2026-03-30 14:09:15 +00:00
|
|
|
package logger
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-31 03:02:11 +00:00
|
|
|
"cursor-api-proxy/internal/config"
|
2026-03-30 14:09:15 +00:00
|
|
|
"cursor-api-proxy/internal/pool"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
cReset = "\x1b[0m"
|
|
|
|
|
cBold = "\x1b[1m"
|
|
|
|
|
cDim = "\x1b[2m"
|
|
|
|
|
cCyan = "\x1b[36m"
|
|
|
|
|
cBCyan = "\x1b[1;96m"
|
|
|
|
|
cGreen = "\x1b[32m"
|
|
|
|
|
cBGreen = "\x1b[1;92m"
|
|
|
|
|
cYellow = "\x1b[33m"
|
|
|
|
|
cMagenta = "\x1b[35m"
|
|
|
|
|
cBMagenta = "\x1b[1;95m"
|
|
|
|
|
cRed = "\x1b[31m"
|
|
|
|
|
cGray = "\x1b[90m"
|
|
|
|
|
cWhite = "\x1b[97m"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var roleStyle = map[string]string{
|
|
|
|
|
"system": cYellow,
|
|
|
|
|
"user": cCyan,
|
|
|
|
|
"assistant": cGreen,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var roleEmoji = map[string]string{
|
|
|
|
|
"system": "🔧",
|
|
|
|
|
"user": "👤",
|
|
|
|
|
"assistant": "🤖",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ts() string {
|
2026-03-31 03:02:11 +00:00
|
|
|
return cGray + time.Now().UTC().Format("15:04:05") + cReset
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func tsDate() string {
|
|
|
|
|
return cGray + time.Now().UTC().Format("2006-01-02 15:04:05") + cReset
|
2026-03-30 14:09:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func truncate(s string, max int) string {
|
|
|
|
|
if len(s) <= max {
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
head := int(float64(max) * 0.6)
|
|
|
|
|
tail := max - head
|
|
|
|
|
omitted := len(s) - head - tail
|
|
|
|
|
return s[:head] + fmt.Sprintf("%s … (%d chars omitted) … ", cDim, omitted) + s[len(s)-tail:] + cReset
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func hr(ch string, length int) string {
|
|
|
|
|
return cGray + strings.Repeat(ch, length) + cReset
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TrafficMessage struct {
|
|
|
|
|
Role string
|
|
|
|
|
Content string
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 03:02:11 +00:00
|
|
|
func LogServerStart(version, scheme, host string, port int, cfg config.BridgeConfig) {
|
|
|
|
|
fmt.Printf("\n%s%s╔══════════════════════════════════════════╗%s\n", cBold, cBCyan, cReset)
|
|
|
|
|
fmt.Printf("%s%s cursor-api-proxy %sv%s%s%s%s ready%s\n",
|
|
|
|
|
cBold, cBCyan, cReset, cBold, cWhite, version, cBCyan, cReset)
|
|
|
|
|
fmt.Printf("%s%s╚══════════════════════════════════════════╝%s\n\n", cBold, cBCyan, cReset)
|
|
|
|
|
url := fmt.Sprintf("%s://%s:%d", scheme, host, port)
|
|
|
|
|
fmt.Printf(" %s●%s listening %s%s%s\n", cBGreen, cReset, cBold, url, cReset)
|
|
|
|
|
fmt.Printf(" %s▸%s agent %s%s%s\n", cCyan, cReset, cDim, cfg.AgentBin, cReset)
|
|
|
|
|
fmt.Printf(" %s▸%s workspace %s%s%s\n", cCyan, cReset, cDim, cfg.Workspace, cReset)
|
|
|
|
|
fmt.Printf(" %s▸%s model %s%s%s\n", cCyan, cReset, cDim, cfg.DefaultModel, cReset)
|
|
|
|
|
fmt.Printf(" %s▸%s mode %s%s%s\n", cCyan, cReset, cDim, cfg.Mode, cReset)
|
|
|
|
|
|
|
|
|
|
flags := []string{}
|
|
|
|
|
if cfg.Force {
|
|
|
|
|
flags = append(flags, "force")
|
|
|
|
|
}
|
|
|
|
|
if cfg.ApproveMcps {
|
|
|
|
|
flags = append(flags, "approve-mcps")
|
|
|
|
|
}
|
|
|
|
|
if cfg.MaxMode {
|
|
|
|
|
flags = append(flags, "max-mode")
|
|
|
|
|
}
|
|
|
|
|
if cfg.Verbose {
|
|
|
|
|
flags = append(flags, "verbose")
|
|
|
|
|
}
|
|
|
|
|
if cfg.ChatOnlyWorkspace {
|
|
|
|
|
flags = append(flags, "chat-only")
|
|
|
|
|
}
|
|
|
|
|
if cfg.RequiredKey != "" {
|
|
|
|
|
flags = append(flags, "api-key-required")
|
|
|
|
|
}
|
|
|
|
|
if len(flags) > 0 {
|
|
|
|
|
fmt.Printf(" %s▸%s flags %s%s%s\n", cCyan, cReset, cYellow, strings.Join(flags, " · "), cReset)
|
|
|
|
|
}
|
|
|
|
|
if len(cfg.ConfigDirs) > 0 {
|
|
|
|
|
fmt.Printf(" %s▸%s pool %s%d accounts%s\n", cCyan, cReset, cBGreen, len(cfg.ConfigDirs), cReset)
|
|
|
|
|
}
|
|
|
|
|
fmt.Println()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LogShutdown(sig string) {
|
|
|
|
|
fmt.Printf("\n%s %s⊘ %s received — shutting down gracefully…%s\n", tsDate(), cYellow, sig, cReset)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 14:09:15 +00:00
|
|
|
func LogIncoming(method, pathname, remoteAddress string) {
|
2026-03-31 03:02:11 +00:00
|
|
|
methodColor := cBCyan
|
|
|
|
|
switch method {
|
|
|
|
|
case "POST":
|
|
|
|
|
methodColor = cBMagenta
|
|
|
|
|
case "GET":
|
|
|
|
|
methodColor = cBCyan
|
|
|
|
|
case "DELETE":
|
|
|
|
|
methodColor = cRed
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("%s %s%s%s%s %s%s%s %s(%s)%s\n",
|
|
|
|
|
ts(),
|
|
|
|
|
methodColor, cBold, method, cReset,
|
|
|
|
|
cWhite, pathname, cReset,
|
|
|
|
|
cDim, remoteAddress, cReset,
|
|
|
|
|
)
|
2026-03-30 14:09:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LogAccountAssigned(configDir string) {
|
|
|
|
|
if configDir == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
name := filepath.Base(configDir)
|
2026-03-31 03:02:11 +00:00
|
|
|
fmt.Printf("%s %s→%s account %s%s%s\n", ts(), cBCyan, cReset, cBold, name, cReset)
|
2026-03-30 14:09:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LogAccountStats(verbose bool, stats []pool.AccountStat) {
|
|
|
|
|
if !verbose || len(stats) == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
now := time.Now().UnixMilli()
|
|
|
|
|
fmt.Printf("%s┌─ Account Stats %s┐%s\n", cGray, strings.Repeat("─", 44), cReset)
|
|
|
|
|
for _, s := range stats {
|
|
|
|
|
name := fmt.Sprintf("%-20s", filepath.Base(s.ConfigDir))
|
2026-03-31 03:02:11 +00:00
|
|
|
active := fmt.Sprintf("%sactive:0%s", cDim, cReset)
|
2026-03-30 14:09:15 +00:00
|
|
|
if s.ActiveRequests > 0 {
|
|
|
|
|
active = fmt.Sprintf("%sactive:%d%s", cBCyan, s.ActiveRequests, cReset)
|
|
|
|
|
}
|
|
|
|
|
total := fmt.Sprintf("total:%s%d%s", cBold, s.TotalRequests, cReset)
|
|
|
|
|
ok := fmt.Sprintf("%sok:%d%s", cGreen, s.TotalSuccess, cReset)
|
2026-03-31 03:02:11 +00:00
|
|
|
errStr := fmt.Sprintf("%serr:0%s", cDim, cReset)
|
2026-03-30 14:09:15 +00:00
|
|
|
if s.TotalErrors > 0 {
|
|
|
|
|
errStr = fmt.Sprintf("%serr:%d%s", cRed, s.TotalErrors, cReset)
|
|
|
|
|
}
|
2026-03-31 03:02:11 +00:00
|
|
|
rl := fmt.Sprintf("%srl:0%s", cDim, cReset)
|
2026-03-30 14:09:15 +00:00
|
|
|
if s.TotalRateLimits > 0 {
|
|
|
|
|
rl = fmt.Sprintf("%srl:%d%s", cYellow, s.TotalRateLimits, cReset)
|
|
|
|
|
}
|
|
|
|
|
avg := "avg:-"
|
|
|
|
|
if s.TotalRequests > 0 {
|
|
|
|
|
avg = fmt.Sprintf("avg:%dms", s.TotalLatencyMs/int64(s.TotalRequests))
|
|
|
|
|
}
|
|
|
|
|
status := fmt.Sprintf("%s✓%s", cGreen, cReset)
|
|
|
|
|
if s.IsRateLimited {
|
|
|
|
|
recovers := time.UnixMilli(s.RateLimitUntil).UTC().Format(time.RFC3339)
|
|
|
|
|
_ = now
|
|
|
|
|
status = fmt.Sprintf("%s⛔ rate-limited (recovers %s)%s", cRed, recovers, cReset)
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf(" %s%s%s %s %s %s %s %s %s%s%s %s\n",
|
|
|
|
|
cBold, name, cReset, active, total, ok, errStr, rl, cDim, avg, cReset, status)
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("%s└%s┘%s\n", cGray, strings.Repeat("─", 60), cReset)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LogTrafficRequest(verbose bool, model string, messages []TrafficMessage, isStream bool) {
|
|
|
|
|
if !verbose {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-31 03:02:11 +00:00
|
|
|
modeTag := fmt.Sprintf("%ssync%s", cDim, cReset)
|
2026-03-30 14:09:15 +00:00
|
|
|
if isStream {
|
|
|
|
|
modeTag = fmt.Sprintf("%s⚡ stream%s", cBCyan, cReset)
|
|
|
|
|
}
|
|
|
|
|
modelStr := fmt.Sprintf("%s✦ %s%s", cBMagenta, model, cReset)
|
|
|
|
|
fmt.Println(hr("─", 60))
|
|
|
|
|
fmt.Printf("%s 📤 %s%sREQUEST%s %s %s\n", ts(), cBCyan, cBold, cReset, modelStr, modeTag)
|
|
|
|
|
for _, m := range messages {
|
|
|
|
|
roleColor := cWhite
|
|
|
|
|
if c, ok := roleStyle[m.Role]; ok {
|
|
|
|
|
roleColor = c
|
|
|
|
|
}
|
|
|
|
|
emoji := "💬"
|
|
|
|
|
if e, ok := roleEmoji[m.Role]; ok {
|
|
|
|
|
emoji = e
|
|
|
|
|
}
|
|
|
|
|
label := fmt.Sprintf("%s%s[%s]%s", roleColor, cBold, m.Role, cReset)
|
|
|
|
|
charCount := fmt.Sprintf("%s(%d chars)%s", cDim, len(m.Content), cReset)
|
|
|
|
|
preview := truncate(strings.ReplaceAll(m.Content, "\n", "↵ "), 280)
|
|
|
|
|
fmt.Printf(" %s %s %s\n", emoji, label, charCount)
|
|
|
|
|
fmt.Printf(" %s%s%s\n", cDim, preview, cReset)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LogTrafficResponse(verbose bool, model, text string, isStream bool) {
|
|
|
|
|
if !verbose {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-31 03:02:11 +00:00
|
|
|
modeTag := fmt.Sprintf("%ssync%s", cDim, cReset)
|
2026-03-30 14:09:15 +00:00
|
|
|
if isStream {
|
|
|
|
|
modeTag = fmt.Sprintf("%s⚡ stream%s", cBGreen, cReset)
|
|
|
|
|
}
|
|
|
|
|
modelStr := fmt.Sprintf("%s✦ %s%s", cBMagenta, model, cReset)
|
|
|
|
|
charCount := fmt.Sprintf("%s%d%s%s chars%s", cBold, len(text), cReset, cDim, cReset)
|
|
|
|
|
preview := truncate(strings.ReplaceAll(text, "\n", "↵ "), 480)
|
|
|
|
|
fmt.Printf("%s 📥 %s%sRESPONSE%s %s %s %s\n", ts(), cBGreen, cBold, cReset, modelStr, modeTag, charCount)
|
|
|
|
|
fmt.Printf(" 🤖 %s%s%s\n", cGreen, preview, cReset)
|
|
|
|
|
fmt.Println(hr("─", 60))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func AppendSessionLine(logPath, method, pathname, remoteAddress string, statusCode int) {
|
|
|
|
|
line := fmt.Sprintf("%s %s %s %s %d\n", time.Now().UTC().Format(time.RFC3339), method, pathname, remoteAddress, statusCode)
|
|
|
|
|
dir := filepath.Dir(logPath)
|
|
|
|
|
if err := os.MkdirAll(dir, 0755); err == nil {
|
|
|
|
|
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
|
|
|
|
if err == nil {
|
|
|
|
|
_, _ = f.WriteString(line)
|
|
|
|
|
f.Close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 03:02:11 +00:00
|
|
|
func LogTruncation(originalLen, finalLen int) {
|
|
|
|
|
fmt.Printf("%s %s⚠ prompt truncated%s %s(%d → %d chars, tail preserved)%s\n",
|
|
|
|
|
ts(), cYellow, cReset, cDim, originalLen, finalLen, cReset)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 14:09:15 +00:00
|
|
|
func LogAgentError(logPath, method, pathname, remoteAddress string, exitCode int, stderr string) string {
|
|
|
|
|
errMsg := fmt.Sprintf("Cursor CLI failed (exit %d): %s", exitCode, strings.TrimSpace(stderr))
|
2026-03-31 03:02:11 +00:00
|
|
|
fmt.Fprintf(os.Stderr, "%s %s✗ agent error%s %s%s%s\n", ts(), cRed, cReset, cDim, errMsg, cReset)
|
2026-03-30 14:09:15 +00:00
|
|
|
truncated := strings.TrimSpace(stderr)
|
|
|
|
|
if len(truncated) > 200 {
|
|
|
|
|
truncated = truncated[:200]
|
|
|
|
|
}
|
|
|
|
|
truncated = strings.ReplaceAll(truncated, "\n", " ")
|
|
|
|
|
line := fmt.Sprintf("%s ERROR %s %s %s agent_exit_%d %s\n",
|
|
|
|
|
time.Now().UTC().Format(time.RFC3339), method, pathname, remoteAddress, exitCode, truncated)
|
|
|
|
|
if f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
|
|
|
|
|
_, _ = f.WriteString(line)
|
|
|
|
|
f.Close()
|
|
|
|
|
}
|
|
|
|
|
return errMsg
|
|
|
|
|
}
|