chore: import Local-Cursor v1.1.8 source snapshot

Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage.
Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
This commit is contained in:
FuDie0915
2026-11-10 01:24:28 +08:00
commit db73cf5a39
337 changed files with 147502 additions and 0 deletions

417
internal/cursor/cli.go Normal file
View File

@@ -0,0 +1,417 @@
// cli.go 管理 CLI 版 cursor-agent 的代理设置注入。
// 通过在数据目录生成 wrapper 脚本和 preload 脚本,
// 将 CLI 的 cursor.sh 连接重定向到后端 HTTPS+HTTP/2 监听器。
package cursor
import (
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"cursor/internal/appdata"
"cursor/internal/logger"
localruntime "cursor/internal/runtime"
)
// CLIWrapperDir 返回 wrapper 脚本存放目录。
func CLIWrapperDir() string {
return filepath.Join(appdata.RootDir(), "cli")
}
// CLIWrapperPath 返回当前平台的 wrapper 脚本完整路径。
func CLIWrapperPath() string {
if runtime.GOOS == "windows" {
return filepath.Join(CLIWrapperDir(), "agent.cmd")
}
return filepath.Join(CLIWrapperDir(), "agent")
}
// DetectCLIInstallPath 尝试检测 CLI 版 cursor-agent 的安装路径。
func DetectCLIInstallPath() string {
homeDir, err := os.UserHomeDir()
if err != nil {
return ""
}
candidates := make([]string, 0, 4)
switch runtime.GOOS {
case "windows":
localAppData := strings.TrimSpace(os.Getenv("LOCALAPPDATA"))
if localAppData == "" {
localAppData = filepath.Join(homeDir, "AppData", "Local")
}
candidates = append(candidates,
filepath.Join(localAppData, "cursor-agent", "agent.cmd"),
)
case "darwin":
candidates = append(candidates,
filepath.Join(homeDir, ".cursor-agent", "agent"),
filepath.Join(homeDir, ".local", "bin", "cursor-agent"),
)
case "linux":
candidates = append(candidates,
filepath.Join(homeDir, ".cursor-agent", "agent"),
filepath.Join(homeDir, ".local", "bin", "cursor-agent"),
"/usr/local/bin/cursor-agent",
)
}
for _, c := range candidates {
if info, err := os.Stat(c); err == nil && !info.IsDir() {
return c
}
}
return ""
}
// cliPreloadFilename 是 preload 脚本的文件名。
const cliPreloadFilename = "tls-bypass.cjs"
// cliPreloadScript 是注入到 Node.js 的 preload 脚本。
//
// 工作原理:
// 1. patch net.connect — 将 cursor.sh 域名的 TCP 连接重定向到本地后端 HTTPS 端口
// 2. patch tls.connect — 强制 rejectUnauthorized=false,使后端签发的 CA 证书被接受
//
// CLI 的 connect-node 使用 HTTP/2 (http2.connect),goproxy 无法 MITM HTTP/2 帧。
// 因此跳过 MITM 代理,直接在 TCP 层重定向到后端的 HTTPS+HTTP/2 监听器。
// 后端 Go http.Server.ServeTLS 原生支持 HTTP/2 ALPN。
const cliPreloadScript = `// Generated by Local-Curosr: redirect cursor.sh TCP to local HTTPS backend
'use strict';
const tls = require('tls');
const net = require('net');
const fs = require('fs');
const path = require('path');
const os = require('os');
const logPath = path.join(os.homedir(), '.cursor-local-assistant-v2', 'logs', 'tls-bypass.log');
function dbg(msg) {
try { fs.appendFileSync(logPath, new Date().toISOString() + ' ' + msg + '\n'); } catch(e) {}
}
var REDIRECT_PORT = parseInt(process.env.CLI_HTTPS_PORT || '39092', 10);
var REDIRECT_HOST = '127.0.0.1';
var REDIRECT_DOMAINS = ['api2.cursor.sh', 'api3.cursor.sh'];
function shouldRedirect(host) {
if (!host) return false;
host = String(host).toLowerCase().trim();
for (var i = 0; i < REDIRECT_DOMAINS.length; i++) {
var d = REDIRECT_DOMAINS[i];
if (host === d || host.endsWith('.' + d)) return true;
}
return false;
}
// --- patch net.connect: TCP 层重定向 ---
var origNetConnect = net.connect;
function patchedNetConnect() {
if (typeof arguments[0] === 'object' && arguments[0] !== null) {
var opts = arguments[0];
if (shouldRedirect(opts.host)) {
dbg('net.connect redirect: ' + opts.host + ':' + opts.port + ' -> ' + REDIRECT_HOST + ':' + REDIRECT_PORT);
opts.host = REDIRECT_HOST;
opts.port = REDIRECT_PORT;
}
} else if (typeof arguments[0] === 'number' && typeof arguments[1] === 'string') {
if (shouldRedirect(arguments[1])) {
dbg('net.connect redirect (positional): ' + arguments[1] + ':' + arguments[0] + ' -> ' + REDIRECT_HOST + ':' + REDIRECT_PORT);
arguments[1] = REDIRECT_HOST;
arguments[0] = REDIRECT_PORT;
}
}
return origNetConnect.apply(this, arguments);
}
net.connect = patchedNetConnect;
net.createConnection = patchedNetConnect;
// --- patch tls.connect: 强制 rejectUnauthorized=false + 重定向 ---
var origTLSConnect = tls.connect;
tls.connect = function() {
for (var i = 0; i < arguments.length && i < 3; i++) {
if (arguments[i] && typeof arguments[i] === 'object' && !Array.isArray(arguments[i])) {
var o = arguments[i];
o.rejectUnauthorized = false;
if (o.host && shouldRedirect(o.host)) {
dbg('tls.connect redirect: ' + o.host + ':' + o.port + ' -> ' + REDIRECT_HOST + ':' + REDIRECT_PORT);
o.host = REDIRECT_HOST;
o.port = REDIRECT_PORT;
if (!o.servername) o.servername = o.host;
}
break;
}
}
return origTLSConnect.apply(this, arguments);
};
dbg('tls-bypass preload loaded, redirect=' + REDIRECT_HOST + ':' + REDIRECT_PORT);
`
// writeCLIPreloadScript 将 preload 脚本写入 wrapper 目录,返回文件路径。
func writeCLIPreloadScript() (string, error) {
wrapperDir := CLIWrapperDir()
if err := os.MkdirAll(wrapperDir, 0o755); err != nil {
return "", fmt.Errorf("创建 preload 目录失败: %w", err)
}
preloadPath := filepath.Join(wrapperDir, cliPreloadFilename)
if err := os.WriteFile(preloadPath, []byte(cliPreloadScript), 0o644); err != nil {
return "", fmt.Errorf("写入 preload 脚本失败: %w", err)
}
return preloadPath, nil
}
// WriteCLIProxyWrapper 在数据目录生成 wrapper 脚本。
//
// wrapper 设置环境变量后调用真实 CLI:
// - CLI_HTTPS_PORT → 后端 HTTPS+HTTP/2 监听端口(preload 脚本读取)
// - NODE_EXTRA_CA_CERTS → CA 证书路径
// - NODE_OPTIONS → --require preload 脚本,patch net.connect/tls.connect
// - CURSOR_AUTH_TOKEN → 注入 token,跳过浏览器登录
//
// 不再设置 HTTP_PROXY/HTTPS_PROXY — 跳过 goproxy MITM,
// 由 preload 脚本在 TCP 层直接重定向到后端 HTTPS 监听器。
func WriteCLIProxyWrapper(cliHTTPSAddr, caCertPath string) error {
cliHTTPSAddr = strings.TrimSpace(cliHTTPSAddr)
caCertPath = strings.TrimSpace(caCertPath)
if cliHTTPSAddr == "" {
return fmt.Errorf("CLI HTTPS 监听地址为空")
}
// 提取端口号
_, port, err := net.SplitHostPort(cliHTTPSAddr)
if err != nil {
return fmt.Errorf("解析 CLI HTTPS 监听地址失败: %w", err)
}
cliPath := DetectCLIInstallPath()
if cliPath == "" {
return fmt.Errorf("未检测到 CLI 版 cursor-agent 安装路径")
}
wrapperDir := CLIWrapperDir()
if err := os.MkdirAll(wrapperDir, 0o755); err != nil {
return fmt.Errorf("创建 wrapper 目录失败: %w", err)
}
preloadPath, err := writeCLIPreloadScript()
if err != nil {
return err
}
authToken := localruntime.InjectAuthToken
var content string
if runtime.GOOS == "windows" {
content = buildWindowsWrapper(port, caCertPath, authToken, cliPath, preloadPath)
} else {
content = buildUnixWrapper(port, caCertPath, authToken, cliPath, preloadPath)
}
wrapperPath := CLIWrapperPath()
if err := os.WriteFile(wrapperPath, []byte(content), 0o755); err != nil {
return fmt.Errorf("写入 wrapper 脚本失败: %w", err)
}
logger.Infof(
"writeCLIProxyWrapper: path=%s https_port=%s ca=%s cli=%s preload=%s",
wrapperPath, port, caCertPath, cliPath, preloadPath,
)
return nil
}
// ClearCLIProxyWrapper 移除 wrapper 脚本、preload 脚本和终端初始化脚本。
func ClearCLIProxyWrapper() error {
wrapperPath := CLIWrapperPath()
if _, err := os.Stat(wrapperPath); err != nil {
if !os.IsNotExist(err) {
return err
}
} else if err := os.Remove(wrapperPath); err != nil {
return fmt.Errorf("移除 wrapper 脚本失败: %w", err)
}
preloadPath := filepath.Join(CLIWrapperDir(), cliPreloadFilename)
if _, err := os.Stat(preloadPath); err != nil {
if !os.IsNotExist(err) {
return err
}
} else if err := os.Remove(preloadPath); err != nil {
return fmt.Errorf("移除 preload 脚本失败: %w", err)
}
// 清理 Windows 终端初始化批处理脚本
terminalInitPath := filepath.Join(CLIWrapperDir(), "cli-terminal-init.bat")
if _, err := os.Stat(terminalInitPath); err != nil {
if !os.IsNotExist(err) {
return err
}
} else if err := os.Remove(terminalInitPath); err != nil {
return fmt.Errorf("移除终端初始化脚本失败: %w", err)
}
logger.Infof("clearCLIProxyWrapper: removed wrapper=%s preload=%s terminal_init=%s", wrapperPath, preloadPath, terminalInitPath)
return nil
}
// CLIWrapperExists 检查 wrapper 是否已存在。
func CLIWrapperExists() bool {
_, err := os.Stat(CLIWrapperPath())
return err == nil
}
// CLIAuthFilePath 返回 CLI 版 auth.json 的路径。
// Windows: %APPDATA%\Cursor\auth.json; Unix: ~/.cursor/auth.json
func CLIAuthFilePath() string {
switch runtime.GOOS {
case "windows":
appData := strings.TrimSpace(os.Getenv("APPDATA"))
if appData == "" {
homeDir, _ := os.UserHomeDir()
appData = filepath.Join(homeDir, "AppData", "Roaming")
}
return filepath.Join(appData, "Cursor", "auth.json")
default:
homeDir, _ := os.UserHomeDir()
return filepath.Join(homeDir, ".cursor", "auth.json")
}
}
// cliAuthData 对应 CLI auth.json 的 JSON 结构。
type cliAuthData struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
APIKey *string `json:"apiKey"`
BedrockCredentials *string `json:"bedrockCredentials"`
}
// cliAuthBackupPath 返回真实凭据的备份路径(auth.json.local-cursor-backup)。
func cliAuthBackupPath() string {
return CLIAuthFilePath() + ".local-cursor-backup"
}
// WriteCLIAuthFile 写入 mock auth.json,使 CLI 的 FileCredentialStore.getAllCredentials() 通过认证检查。
// accessToken/refreshToken 使用 InjectAuthToken(有效 JWT,exp 在 2099 年)。
// 覆盖前会备份用户的真实凭据(仅首次,之后保留最早那份原始备份),
// ClearCLIAuthFile 时若存在备份则恢复,避免销毁用户真实登录态。
func WriteCLIAuthFile() error {
authPath := CLIAuthFilePath()
dir := filepath.Dir(authPath)
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("创建 auth.json 目录失败: %w", err)
}
// 首次覆盖前备份真实凭据;备份已存在说明此前已备份过,不覆盖最早的原件。
backupPath := cliAuthBackupPath()
if raw, err := os.ReadFile(authPath); err == nil {
if _, statErr := os.Stat(backupPath); os.IsNotExist(statErr) {
if isMockAuthData(raw) {
logger.Infof("writeCLIAuthFile: existing auth.json is mock, skip backup path=%s", authPath)
} else if writeErr := os.WriteFile(backupPath, raw, 0o600); writeErr != nil {
return fmt.Errorf("备份原 auth.json 失败: %w", writeErr)
} else {
logger.Infof("writeCLIAuthFile: backed up original auth.json to %s", backupPath)
}
}
}
data := cliAuthData{
AccessToken: localruntime.InjectAuthToken,
RefreshToken: localruntime.InjectAuthToken,
APIKey: nil,
BedrockCredentials: nil,
}
content, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("序列化 auth.json 失败: %w", err)
}
if err := os.WriteFile(authPath, content, 0o600); err != nil {
return fmt.Errorf("写入 auth.json 失败: %w", err)
}
logger.Infof("writeCLIAuthFile: path=%s", authPath)
return nil
}
// isMockAuthData 判断 auth.json 内容是否为本工具写入的 mock 凭据。
func isMockAuthData(raw []byte) bool {
var data cliAuthData
if err := json.Unmarshal(raw, &data); err != nil {
return false
}
return data.AccessToken == localruntime.InjectAuthToken
}
// ClearCLIAuthFile 移除 mock auth.json。
// 若存在真实凭据备份则恢复之;仅当 auth.json 是 mock 内容时才删除。
func ClearCLIAuthFile() error {
authPath := CLIAuthFilePath()
raw, err := os.ReadFile(authPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
backupPath := cliAuthBackupPath()
backupExists := false
if _, statErr := os.Stat(backupPath); statErr == nil {
backupExists = true
}
if isMockAuthData(raw) {
if backupExists {
// 恢复用户真实凭据。
if restoreErr := os.Rename(backupPath, authPath); restoreErr != nil {
return fmt.Errorf("恢复原 auth.json 失败: %w", restoreErr)
}
logger.Infof("clearCLIAuthFile: restored original auth.json from %s", backupPath)
return nil
}
if err := os.Remove(authPath); err != nil {
return fmt.Errorf("移除 auth.json 失败: %w", err)
}
logger.Infof("clearCLIAuthFile: removed mock auth.json path=%s", authPath)
return nil
}
// 内容不是 mock(用户在此期间重新登录过),保留现状并不动备份。
logger.Infof("clearCLIAuthFile: auth.json is not mock, leave untouched path=%s", authPath)
return nil
}
func buildWindowsWrapper(port, caCertPath, authToken, cliPath, preloadPath string) string {
cliDir := filepath.Dir(cliPath)
psScript := filepath.Join(cliDir, "cursor-agent.ps1")
return fmt.Sprintf(`@echo off
setlocal
set "HTTP_PROXY="
set "HTTPS_PROXY="
set "CLI_HTTPS_PORT=%s"
set "NODE_EXTRA_CA_CERTS=%s"
set "NODE_TLS_REJECT_UNAUTHORIZED=0"
set "NODE_OPTIONS=--require=%s"
set "CURSOR_AUTH_TOKEN=%s"
set "CURSOR_INVOKED_AS=agent.cmd"
%%SystemRoot%%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%s" %%*
`, port, caCertPath, preloadPath, authToken, psScript)
}
func buildUnixWrapper(port, caCertPath, authToken, cliPath, preloadPath string) string {
return fmt.Sprintf(`#!/bin/sh
unset HTTP_PROXY
unset HTTPS_PROXY
export CLI_HTTPS_PORT="%s"
export NODE_EXTRA_CA_CERTS="%s"
export NODE_TLS_REJECT_UNAUTHORIZED=0
export NODE_OPTIONS="--require=%s"
export CURSOR_AUTH_TOKEN="%s"
exec "%s" "$@"
`, port, caCertPath, preloadPath, authToken, cliPath)
}