opencode-cursor-agent/internal/env/env.go

332 lines
8.2 KiB
Go

package env
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
)
type EnvSource map[string]string
type LoadedEnv struct {
AgentBin string
AgentNode string
AgentScript string
CommandShell string
Host string
Port int
RequiredKey string
DefaultModel 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
}
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
}
func OsEnvToMap() EnvSource {
m := make(EnvSource)
for _, kv := range os.Environ() {
parts := strings.SplitN(kv, "=", 2)
if len(parts) == 2 {
m[parts[0]] = parts[1]
}
}
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
}
return LoadedEnv{
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"})),
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,
}
}
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()}
}