mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 19:47:10 +08:00
Implemented the buildShellToolCallDeltaMessage function to map client shell output to the delta format for Cursor's terminal bubble. This includes handling both stdout and stderr events. Updated the service to publish these messages when processing execution results. Additionally, added a test for enabling terminal output UI streaming in the bootstrap statsig configuration.
223 lines
6.1 KiB
Go
223 lines
6.1 KiB
Go
package cursor
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
|
|
"cursor/internal/logger"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
const (
|
|
cursorStateMembershipType = "ultra"
|
|
cursorStateSubscriptionStatus = "active"
|
|
cursorStateDefaultSignUpType = "Google"
|
|
cursorStateSQLiteBusyTimeoutMS = 2000
|
|
cursorStateDBRelativePath = "Cursor/User/globalStorage/state.vscdb"
|
|
cursorStateDarwinRelativePath = "Library/Application Support/Cursor/User/globalStorage/state.vscdb"
|
|
cursorStateLinuxRelativePath = ".config/Cursor/User/globalStorage/state.vscdb"
|
|
cursorStateStatsigBootstrapKey = "workbench.experiments.statsigBootstrap"
|
|
)
|
|
|
|
var cursorStateDisabledStatsigGates = []string{
|
|
"decompose_always_local_ext_host",
|
|
"cursor_extensions_isolation_v2",
|
|
"disable_terminal_output_ui_streaming",
|
|
}
|
|
|
|
// InjectCursorUserInfo synchronizes the Cursor user-level auth cache used by the
|
|
// Settings page. It does not modify the installed Cursor app bundle.
|
|
func InjectCursorUserInfo(email, token string) error {
|
|
stateDBPath, err := resolveCursorStateDBPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(stateDBPath), 0o755); err != nil {
|
|
return fmt.Errorf("创建 Cursor 状态目录失败: %w", err)
|
|
}
|
|
|
|
values := buildCursorAuthStateValues(email, token)
|
|
if err := syncCursorAuthStateDB(stateDBPath, values); err != nil {
|
|
return fmt.Errorf("同步 Cursor 状态库失败 path=%s: %w", stateDBPath, err)
|
|
}
|
|
|
|
logger.Infof(
|
|
"injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s disabled_statsig_gates=%s",
|
|
stateDBPath,
|
|
values["cursorAuth/cachedEmail"],
|
|
values["cursorAuth/stripeMembershipType"],
|
|
values["cursorAuth/stripeSubscriptionStatus"],
|
|
strings.Join(cursorStateDisabledStatsigGates, ","),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
func buildCursorAuthStateValues(email, token string) map[string]string {
|
|
email = strings.TrimSpace(email)
|
|
token = strings.TrimSpace(token)
|
|
|
|
return map[string]string{
|
|
"cursorAuth/accessToken": token,
|
|
"cursorAuth/cachedEmail": email,
|
|
"cursorAuth/cachedSignUpType": cursorStateDefaultSignUpType,
|
|
"cursorAuth/refreshToken": token,
|
|
"cursorAuth/stripeMembershipType": cursorStateMembershipType,
|
|
"cursorAuth/stripeSubscriptionStatus": cursorStateSubscriptionStatus,
|
|
}
|
|
}
|
|
|
|
func syncCursorAuthStateDB(path string, values map[string]string) error {
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
db.SetMaxOpenConns(1)
|
|
db.SetMaxIdleConns(1)
|
|
|
|
ctx := context.Background()
|
|
if _, err := db.ExecContext(ctx, fmt.Sprintf("PRAGMA busy_timeout = %d", cursorStateSQLiteBusyTimeoutMS)); err != nil {
|
|
return err
|
|
}
|
|
if _, err := db.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)"); err != nil {
|
|
return err
|
|
}
|
|
|
|
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
committed := false
|
|
defer func() {
|
|
if !committed {
|
|
_ = tx.Rollback()
|
|
}
|
|
}()
|
|
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
stmt, err := tx.PrepareContext(ctx, "INSERT OR REPLACE INTO ItemTable(key, value) VALUES(?, ?)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
for _, key := range keys {
|
|
if _, err := stmt.ExecContext(ctx, key, values[key]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := disableCursorStatsigGates(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
committed = true
|
|
return nil
|
|
}
|
|
|
|
func disableCursorStatsigGates(ctx context.Context, tx *sql.Tx) error {
|
|
var raw []byte
|
|
err := tx.QueryRowContext(ctx, "SELECT value FROM ItemTable WHERE key = ?", cursorStateStatsigBootstrapKey).Scan(&raw)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
return fmt.Errorf("解析 Cursor Statsig bootstrap 失败: %w", err)
|
|
}
|
|
|
|
featureGates, _ := payload["feature_gates"].(map[string]any)
|
|
if featureGates == nil {
|
|
featureGates = map[string]any{}
|
|
payload["feature_gates"] = featureGates
|
|
}
|
|
|
|
hashUsed, _ := payload["hash_used"].(string)
|
|
for _, gate := range cursorStateDisabledStatsigGates {
|
|
disableCursorStatsigGate(featureGates, gate)
|
|
if strings.EqualFold(hashUsed, "djb2") {
|
|
disableCursorStatsigGate(featureGates, cursorStateDJB2Hash(gate))
|
|
}
|
|
}
|
|
|
|
updated, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("编码 Cursor Statsig bootstrap 失败: %w", err)
|
|
}
|
|
if _, err := tx.ExecContext(ctx, "UPDATE ItemTable SET value = ? WHERE key = ?", updated, cursorStateStatsigBootstrapKey); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func disableCursorStatsigGate(featureGates map[string]any, key string) {
|
|
gate, _ := featureGates[key].(map[string]any)
|
|
if gate == nil {
|
|
gate = map[string]any{
|
|
"name": key,
|
|
"rule_id": "local_disabled",
|
|
"ruleID": "local_disabled",
|
|
"group_name": "local_disabled",
|
|
"groupName": "local_disabled",
|
|
"id_type": "userID",
|
|
"idType": "userID",
|
|
}
|
|
featureGates[key] = gate
|
|
}
|
|
gate["value"] = false
|
|
}
|
|
|
|
func cursorStateDJB2Hash(value string) string {
|
|
var hash uint32
|
|
for _, b := range []byte(value) {
|
|
hash = hash*31 + uint32(b)
|
|
}
|
|
return fmt.Sprintf("%d", hash)
|
|
}
|
|
|
|
func resolveCursorStateDBPath() (string, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("获取用户目录失败: %w", err)
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return filepath.Join(homeDir, filepath.FromSlash(cursorStateDarwinRelativePath)), nil
|
|
case "windows":
|
|
appData := strings.TrimSpace(os.Getenv("APPDATA"))
|
|
if appData == "" {
|
|
appData = filepath.Join(homeDir, "AppData", "Roaming")
|
|
}
|
|
return filepath.Join(appData, "Cursor", "User", "globalStorage", "state.vscdb"), nil
|
|
case "linux":
|
|
configDir := strings.TrimSpace(os.Getenv("XDG_CONFIG_HOME"))
|
|
if configDir == "" {
|
|
return filepath.Join(homeDir, filepath.FromSlash(cursorStateLinuxRelativePath)), nil
|
|
}
|
|
return filepath.Join(configDir, filepath.FromSlash(cursorStateDBRelativePath)), nil
|
|
default:
|
|
return "", fmt.Errorf("不支持的系统: %s", runtime.GOOS)
|
|
}
|
|
}
|