2026-03-30 14:09:15 +00:00
|
|
|
package env
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"runtime"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type EnvSource map[string]string
|
|
|
|
|
|
|
|
|
|
type LoadedEnv struct {
|
2026-04-02 14:45:41 +00:00
|
|
|
AgentBin string
|
|
|
|
|
AgentNode string
|
|
|
|
|
AgentScript string
|
|
|
|
|
CommandShell string
|
|
|
|
|
Host string
|
|
|
|
|
Port int
|
|
|
|
|
RequiredKey string
|
|
|
|
|
DefaultModel string
|
|
|
|
|
Provider string
|
|
|
|
|
Force bool
|
|
|
|
|
ApproveMcps bool
|
|
|
|
|
StrictModel bool
|
|
|
|
|
Workspace string
|
|
|
|
|
TimeoutMs int
|
|
|
|
|
TLSCertPath string
|
|
|
|
|
TLSKeyPath string
|
|
|
|
|
SessionsLogPath string
|
|
|
|
|
ChatOnlyWorkspace bool
|
|
|
|
|
Verbose bool
|
|
|
|
|
MaxMode bool
|
|
|
|
|
ConfigDirs []string
|
|
|
|
|
MultiPort bool
|
|
|
|
|
WinCmdlineMax int
|
|
|
|
|
GeminiAccountDir string
|
|
|
|
|
GeminiBrowserVisible bool
|
|
|
|
|
GeminiMaxSessions int
|
2026-03-30 14:09:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AgentCommand struct {
|
|
|
|
|
Command string
|
|
|
|
|
Args []string
|
|
|
|
|
Env map[string]string
|
|
|
|
|
WindowsVerbatimArguments bool
|
|
|
|
|
AgentScriptPath string
|
|
|
|
|
ConfigDir string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getEnvVal(e EnvSource, names []string) string {
|
|
|
|
|
for _, name := range names {
|
|
|
|
|
if v, ok := e[name]; ok && strings.TrimSpace(v) != "" {
|
|
|
|
|
return strings.TrimSpace(v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func envBool(e EnvSource, names []string, def bool) bool {
|
|
|
|
|
raw := getEnvVal(e, names)
|
|
|
|
|
if raw == "" {
|
|
|
|
|
return def
|
|
|
|
|
}
|
|
|
|
|
switch strings.ToLower(raw) {
|
|
|
|
|
case "1", "true", "yes", "on":
|
|
|
|
|
return true
|
|
|
|
|
case "0", "false", "no", "off":
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return def
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func envInt(e EnvSource, names []string, def int) int {
|
|
|
|
|
raw := getEnvVal(e, names)
|
|
|
|
|
if raw == "" {
|
|
|
|
|
return def
|
|
|
|
|
}
|
|
|
|
|
v, err := strconv.Atoi(raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return def
|
|
|
|
|
}
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func normalizeModelId(raw string) string {
|
|
|
|
|
raw = strings.TrimSpace(raw)
|
|
|
|
|
if raw == "" {
|
|
|
|
|
return "auto"
|
|
|
|
|
}
|
|
|
|
|
parts := strings.Split(raw, "/")
|
|
|
|
|
last := parts[len(parts)-1]
|
|
|
|
|
if last == "" {
|
|
|
|
|
return "auto"
|
|
|
|
|
}
|
|
|
|
|
return last
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resolveAbs(raw, cwd string) string {
|
|
|
|
|
if raw == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if filepath.IsAbs(raw) {
|
|
|
|
|
return raw
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(cwd, raw)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isAuthenticatedAccountDir(dir string) bool {
|
|
|
|
|
data, err := os.ReadFile(filepath.Join(dir, "cli-config.json"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
var cfg struct {
|
|
|
|
|
AuthInfo *struct {
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
} `json:"authInfo"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return cfg.AuthInfo != nil && cfg.AuthInfo.Email != ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func discoverAccountDirs(homeDir string) []string {
|
|
|
|
|
if homeDir == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
accountsDir := filepath.Join(homeDir, ".cursor-api-proxy", "accounts")
|
|
|
|
|
entries, err := os.ReadDir(accountsDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var dirs []string
|
|
|
|
|
for _, e := range entries {
|
|
|
|
|
if !e.IsDir() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
dir := filepath.Join(accountsDir, e.Name())
|
|
|
|
|
if isAuthenticatedAccountDir(dir) {
|
|
|
|
|
dirs = append(dirs, dir)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return dirs
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 03:02:11 +00:00
|
|
|
func parseDotEnv(path string) EnvSource {
|
|
|
|
|
data, err := os.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
m := make(EnvSource)
|
|
|
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
|
|
|
line = strings.TrimSpace(line)
|
|
|
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
parts := strings.SplitN(line, "=", 2)
|
|
|
|
|
if len(parts) == 2 {
|
|
|
|
|
m[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func OsEnvToMap(cwdHint ...string) EnvSource {
|
2026-03-30 14:09:15 +00:00
|
|
|
m := make(EnvSource)
|
|
|
|
|
for _, kv := range os.Environ() {
|
|
|
|
|
parts := strings.SplitN(kv, "=", 2)
|
|
|
|
|
if len(parts) == 2 {
|
|
|
|
|
m[parts[0]] = parts[1]
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 03:02:11 +00:00
|
|
|
|
|
|
|
|
cwd := ""
|
|
|
|
|
if len(cwdHint) > 0 && cwdHint[0] != "" {
|
|
|
|
|
cwd = cwdHint[0]
|
|
|
|
|
} else {
|
|
|
|
|
cwd, _ = os.Getwd()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if dotenv := parseDotEnv(filepath.Join(cwd, ".env")); dotenv != nil {
|
|
|
|
|
for k, v := range dotenv {
|
|
|
|
|
if _, exists := m[k]; !exists {
|
|
|
|
|
m[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 14:09:15 +00:00
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func LoadEnvConfig(e EnvSource, cwd string) LoadedEnv {
|
|
|
|
|
if e == nil {
|
|
|
|
|
e = OsEnvToMap()
|
|
|
|
|
}
|
|
|
|
|
if cwd == "" {
|
|
|
|
|
var err error
|
|
|
|
|
cwd, err = os.Getwd()
|
|
|
|
|
if err != nil {
|
|
|
|
|
cwd = "."
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
host := getEnvVal(e, []string{"CURSOR_BRIDGE_HOST"})
|
|
|
|
|
if host == "" {
|
|
|
|
|
host = "127.0.0.1"
|
|
|
|
|
}
|
|
|
|
|
port := envInt(e, []string{"CURSOR_BRIDGE_PORT"}, 8765)
|
|
|
|
|
if port <= 0 {
|
|
|
|
|
port = 8765
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
home := getEnvVal(e, []string{"HOME", "USERPROFILE"})
|
|
|
|
|
|
|
|
|
|
sessionsLogPath := func() string {
|
|
|
|
|
if p := resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_SESSIONS_LOG"}), cwd); p != "" {
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
if home != "" {
|
|
|
|
|
return filepath.Join(home, ".cursor-api-proxy", "sessions.log")
|
|
|
|
|
}
|
|
|
|
|
return filepath.Join(cwd, "sessions.log")
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
var configDirs []string
|
|
|
|
|
if raw := getEnvVal(e, []string{"CURSOR_CONFIG_DIRS", "CURSOR_ACCOUNT_DIRS"}); raw != "" {
|
|
|
|
|
for _, d := range strings.Split(raw, ",") {
|
|
|
|
|
d = strings.TrimSpace(d)
|
|
|
|
|
if d != "" {
|
|
|
|
|
if p := resolveAbs(d, cwd); p != "" {
|
|
|
|
|
configDirs = append(configDirs, p)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(configDirs) == 0 {
|
|
|
|
|
configDirs = discoverAccountDirs(home)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
winMax := envInt(e, []string{"CURSOR_BRIDGE_WIN_CMDLINE_MAX"}, 30000)
|
|
|
|
|
if winMax < 4096 {
|
|
|
|
|
winMax = 4096
|
|
|
|
|
}
|
|
|
|
|
if winMax > 32700 {
|
|
|
|
|
winMax = 32700
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
agentBin := getEnvVal(e, []string{"CURSOR_AGENT_BIN", "CURSOR_CLI_BIN", "CURSOR_CLI_PATH"})
|
|
|
|
|
if agentBin == "" {
|
|
|
|
|
agentBin = "agent"
|
|
|
|
|
}
|
|
|
|
|
commandShell := getEnvVal(e, []string{"COMSPEC"})
|
|
|
|
|
if commandShell == "" {
|
|
|
|
|
commandShell = "cmd.exe"
|
|
|
|
|
}
|
|
|
|
|
workspace := resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_WORKSPACE"}), cwd)
|
|
|
|
|
if workspace == "" {
|
|
|
|
|
workspace = cwd
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 14:45:41 +00:00
|
|
|
geminiAccountDir := getEnvVal(e, []string{"GEMINI_ACCOUNT_DIR"})
|
|
|
|
|
if geminiAccountDir == "" {
|
|
|
|
|
geminiAccountDir = filepath.Join(home, ".cursor-api-proxy", "gemini-accounts")
|
|
|
|
|
} else {
|
|
|
|
|
geminiAccountDir = resolveAbs(geminiAccountDir, cwd)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 14:09:15 +00:00
|
|
|
return LoadedEnv{
|
2026-04-02 14:45:41 +00:00
|
|
|
AgentBin: agentBin,
|
|
|
|
|
AgentNode: getEnvVal(e, []string{"CURSOR_AGENT_NODE"}),
|
|
|
|
|
AgentScript: getEnvVal(e, []string{"CURSOR_AGENT_SCRIPT"}),
|
|
|
|
|
CommandShell: commandShell,
|
|
|
|
|
Host: host,
|
|
|
|
|
Port: port,
|
|
|
|
|
RequiredKey: getEnvVal(e, []string{"CURSOR_BRIDGE_API_KEY"}),
|
|
|
|
|
DefaultModel: normalizeModelId(getEnvVal(e, []string{"CURSOR_BRIDGE_DEFAULT_MODEL"})),
|
|
|
|
|
Provider: getEnvVal(e, []string{"CURSOR_BRIDGE_PROVIDER"}),
|
|
|
|
|
Force: envBool(e, []string{"CURSOR_BRIDGE_FORCE"}, false),
|
|
|
|
|
ApproveMcps: envBool(e, []string{"CURSOR_BRIDGE_APPROVE_MCPS"}, false),
|
|
|
|
|
StrictModel: envBool(e, []string{"CURSOR_BRIDGE_STRICT_MODEL"}, true),
|
|
|
|
|
Workspace: workspace,
|
|
|
|
|
TimeoutMs: envInt(e, []string{"CURSOR_BRIDGE_TIMEOUT_MS"}, 300000),
|
|
|
|
|
TLSCertPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_CERT"}), cwd),
|
|
|
|
|
TLSKeyPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_KEY"}), cwd),
|
|
|
|
|
SessionsLogPath: sessionsLogPath,
|
|
|
|
|
ChatOnlyWorkspace: envBool(e, []string{"CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE"}, true),
|
|
|
|
|
Verbose: envBool(e, []string{"CURSOR_BRIDGE_VERBOSE"}, false),
|
|
|
|
|
MaxMode: envBool(e, []string{"CURSOR_BRIDGE_MAX_MODE"}, false),
|
|
|
|
|
ConfigDirs: configDirs,
|
|
|
|
|
MultiPort: envBool(e, []string{"CURSOR_BRIDGE_MULTI_PORT"}, false),
|
|
|
|
|
WinCmdlineMax: winMax,
|
|
|
|
|
GeminiAccountDir: geminiAccountDir,
|
|
|
|
|
GeminiBrowserVisible: envBool(e, []string{"GEMINI_BROWSER_VISIBLE"}, false),
|
|
|
|
|
GeminiMaxSessions: envInt(e, []string{"GEMINI_MAX_SESSIONS"}, 3),
|
2026-03-30 14:09:15 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ResolveAgentCommand(cmd string, args []string, e EnvSource, cwd string) AgentCommand {
|
|
|
|
|
if e == nil {
|
|
|
|
|
e = OsEnvToMap()
|
|
|
|
|
}
|
|
|
|
|
loaded := LoadEnvConfig(e, cwd)
|
|
|
|
|
|
|
|
|
|
cloneEnv := func() map[string]string {
|
|
|
|
|
m := make(map[string]string, len(e))
|
|
|
|
|
for k, v := range e {
|
|
|
|
|
m[k] = v
|
|
|
|
|
}
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
if loaded.AgentNode != "" && loaded.AgentScript != "" {
|
|
|
|
|
agentScriptPath := loaded.AgentScript
|
|
|
|
|
if !filepath.IsAbs(agentScriptPath) {
|
|
|
|
|
agentScriptPath = filepath.Join(cwd, agentScriptPath)
|
|
|
|
|
}
|
|
|
|
|
agentDir := filepath.Dir(agentScriptPath)
|
|
|
|
|
configDir := filepath.Join(agentDir, "..", "data", "config")
|
|
|
|
|
env2 := cloneEnv()
|
|
|
|
|
env2["CURSOR_INVOKED_AS"] = "agent.cmd"
|
|
|
|
|
ac := AgentCommand{
|
|
|
|
|
Command: loaded.AgentNode,
|
|
|
|
|
Args: append([]string{loaded.AgentScript}, args...),
|
|
|
|
|
Env: env2,
|
|
|
|
|
AgentScriptPath: agentScriptPath,
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(filepath.Join(configDir, "cli-config.json")); err == nil {
|
|
|
|
|
ac.ConfigDir = configDir
|
|
|
|
|
}
|
|
|
|
|
return ac
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if strings.HasSuffix(strings.ToLower(cmd), ".cmd") {
|
|
|
|
|
cmdResolved := cmd
|
|
|
|
|
if !filepath.IsAbs(cmd) {
|
|
|
|
|
cmdResolved = filepath.Join(cwd, cmd)
|
|
|
|
|
}
|
|
|
|
|
dir := filepath.Dir(cmdResolved)
|
|
|
|
|
nodeBin := filepath.Join(dir, "node.exe")
|
|
|
|
|
script := filepath.Join(dir, "index.js")
|
|
|
|
|
if _, err1 := os.Stat(nodeBin); err1 == nil {
|
|
|
|
|
if _, err2 := os.Stat(script); err2 == nil {
|
|
|
|
|
configDir := filepath.Join(dir, "..", "data", "config")
|
|
|
|
|
env2 := cloneEnv()
|
|
|
|
|
env2["CURSOR_INVOKED_AS"] = "agent.cmd"
|
|
|
|
|
ac := AgentCommand{
|
|
|
|
|
Command: nodeBin,
|
|
|
|
|
Args: append([]string{script}, args...),
|
|
|
|
|
Env: env2,
|
|
|
|
|
AgentScriptPath: script,
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(filepath.Join(configDir, "cli-config.json")); err == nil {
|
|
|
|
|
ac.ConfigDir = configDir
|
|
|
|
|
}
|
|
|
|
|
return ac
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
quotedArgs := make([]string, len(args))
|
|
|
|
|
for i, a := range args {
|
|
|
|
|
if strings.Contains(a, " ") {
|
|
|
|
|
quotedArgs[i] = `"` + a + `"`
|
|
|
|
|
} else {
|
|
|
|
|
quotedArgs[i] = a
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
cmdLine := `""` + cmd + `" ` + strings.Join(quotedArgs, " ") + `"`
|
|
|
|
|
return AgentCommand{
|
|
|
|
|
Command: loaded.CommandShell,
|
|
|
|
|
Args: []string{"/d", "/s", "/c", cmdLine},
|
|
|
|
|
Env: cloneEnv(),
|
|
|
|
|
WindowsVerbatimArguments: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return AgentCommand{Command: cmd, Args: args, Env: cloneEnv()}
|
|
|
|
|
}
|