mirror of
https://github.com/everestmz/cursor-rpc.git
synced 2025-09-27 03:15:56 +08:00
Regenerate, add client lib helper code, update README
This commit is contained in:
2
Makefile
2
Makefile
@@ -1,5 +1,5 @@
|
||||
extract-schema:
|
||||
go run cmd/extract
|
||||
go run ./cmd/extract
|
||||
|
||||
generate:
|
||||
cd cursor && buf generate
|
||||
|
43
README.md
43
README.md
@@ -6,7 +6,48 @@ Works by reverse-engineering the minified, obfuscated JS in Cursor's VSCode fork
|
||||
|
||||
## Usage
|
||||
|
||||
TODO
|
||||
### Generating client libraries
|
||||
|
||||
Use the .proto files in `./cursor/aiserver/v1` to generate an RPC client library for your language.
|
||||
|
||||
### Go library
|
||||
|
||||
For detailed usage, check out [the basic example](cmd/example/main.go).
|
||||
|
||||
```go
|
||||
// Get default credentials for Cursor:
|
||||
// NOTE: you will need to open Cursor and log in at least once. You may need to re-login to
|
||||
// refresh these credentials, or use the RefreshToken to get a new AccessToken.
|
||||
creds, err := cursor.GetDefaultCredentials()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Set up a service:
|
||||
aiService := cursor.NewAiServiceClient()
|
||||
|
||||
// Get completions!
|
||||
model := "gpt-4"
|
||||
resp, err := aiService.StreamChat(context.TODO(), cursor.NewRequest(creds, &aiserverv1.GetChatRequest{
|
||||
ModelDetails: &aiserverv1.ModelDetails{
|
||||
ModelName: &model,
|
||||
},
|
||||
Conversation: []*aiserverv1.ConversationMessage{
|
||||
{
|
||||
Text: "Hello, who are you?",
|
||||
Type: aiserverv1.ConversationMessage_MESSAGE_TYPE_HUMAN,
|
||||
},
|
||||
},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for resp.Receive() {
|
||||
next := resp.Msg()
|
||||
fmt.Printf(next.Text)
|
||||
}
|
||||
```
|
||||
|
||||
## Updating the schema
|
||||
|
||||
|
163
client.go
Normal file
163
client.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package cursor
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/everestmz/cursor-rpc/cursor/gen/aiserver/v1/aiserverv1connect"
|
||||
)
|
||||
|
||||
const CursorStateDbPath = "User/globalStorage/state.vscdb"
|
||||
|
||||
//go:embed cursor_version.txt
|
||||
var cursorVersion string
|
||||
|
||||
func GetCursorVersion() string {
|
||||
return strings.TrimSpace(cursorVersion)
|
||||
}
|
||||
|
||||
type CursorCredentials struct {
|
||||
AccessToken string `json:"authToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
}
|
||||
|
||||
func generateChecksum(machineID string) string {
|
||||
// Get current timestamp and convert to uint64
|
||||
timestamp := uint64(time.Now().UnixNano() / 1e6)
|
||||
|
||||
// Convert timestamp to 6-byte array
|
||||
timestampBytes := []byte{
|
||||
byte(timestamp >> 40),
|
||||
byte(timestamp >> 32),
|
||||
byte(timestamp >> 24),
|
||||
byte(timestamp >> 16),
|
||||
byte(timestamp >> 8),
|
||||
byte(timestamp),
|
||||
}
|
||||
|
||||
// Apply rolling XOR encryption (function S in the original code)
|
||||
encryptedBytes := encryptBytes(timestampBytes)
|
||||
|
||||
// Convert to base64
|
||||
base64Encoded := base64.StdEncoding.EncodeToString(encryptedBytes)
|
||||
|
||||
// Concatenate with machineID
|
||||
return fmt.Sprintf("%s%s", base64Encoded, machineID)
|
||||
}
|
||||
|
||||
func encryptBytes(input []byte) []byte {
|
||||
w := byte(165)
|
||||
for i := 0; i < len(input); i++ {
|
||||
input[i] = (input[i] ^ w) + byte(i%256)
|
||||
w = input[i]
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func NewRequest[T any](credentials *CursorCredentials, message *T) *connect.Request[T] {
|
||||
req := connect.NewRequest(message)
|
||||
|
||||
req.Header().Set("authorization", "bearer "+credentials.AccessToken)
|
||||
req.Header().Set("x-cursor-client-version", GetCursorVersion())
|
||||
// It doesn't look like the checksum matters. Just that we need one?
|
||||
// Either way, this is the algorithm used. I just don't know what the arg is.
|
||||
req.Header().Set("x-cursor-checksum", generateChecksum("hi"))
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func GetBaseURL() string {
|
||||
// TODO: round robin other base URLs/do fallbacks
|
||||
return "https://api2.cursor.sh"
|
||||
}
|
||||
|
||||
func GetRepoClientURL() string {
|
||||
// TODO: work out why this one is different
|
||||
return "https://repo42.cursor.sh"
|
||||
}
|
||||
|
||||
func GetCursorDir() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return filepath.Join(homeDir, "Library/Application Support/Cursor"), nil
|
||||
default:
|
||||
return "", fmt.Errorf("Unsure what the cursor directory is for GOOS %s - please fix if you know!", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func GetStateDb() (string, error) {
|
||||
cursorDir, err := GetCursorDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(cursorDir, CursorStateDbPath), nil
|
||||
}
|
||||
|
||||
func GetDefaultCredentials() (*CursorCredentials, error) {
|
||||
stateDb, err := GetStateDb()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// XXX: it's debatable whether this is good, but the alternatives are either:
|
||||
// - mattn/sqlite3, which uses CGO. I don't want to force CGO on someone, since this is a lib
|
||||
// - ncruces/go-sqlite3, which doesn't use CGO, but brings a whole wasm runtime
|
||||
// - cznic/sqlite, which uses a ton of unsafe pointers
|
||||
//
|
||||
// At the end of the day, we just need to run this once on app startup, and most local machines
|
||||
// have `sqlite3` installed. /shrug
|
||||
//
|
||||
// If the user wants to do this another way they can just provide creds via the CursorCredentials type
|
||||
|
||||
var getKey = func(key string) (string, error) {
|
||||
cmd := exec.Command("sqlite3", stateDb, fmt.Sprintf(`SELECT value FROM ItemTable WHERE key = '%s';`, key))
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting %s (cmd %s): %w", key, cmd.String(), err)
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
accessToken, err := getKey("cursorAuth/accessToken")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refreshToken, err := getKey("cursorAuth/refreshToken")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CursorCredentials{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewRepositoryServiceClient() aiserverv1connect.RepositoryServiceClient {
|
||||
return aiserverv1connect.NewRepositoryServiceClient(
|
||||
http.DefaultClient,
|
||||
GetRepoClientURL(),
|
||||
)
|
||||
}
|
||||
|
||||
func NewAiServiceClient() aiserverv1connect.AiServiceClient {
|
||||
return aiserverv1connect.NewAiServiceClient(
|
||||
http.DefaultClient,
|
||||
GetBaseURL(),
|
||||
)
|
||||
}
|
60
cmd/example/main.go
Normal file
60
cmd/example/main.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/everestmz/cursor-rpc"
|
||||
aiserverv1 "github.com/everestmz/cursor-rpc/cursor/gen/aiserver/v1"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Get default credentials from cursor
|
||||
creds, err := cursor.GetDefaultCredentials()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Set up the service
|
||||
aiService := cursor.NewAiServiceClient()
|
||||
|
||||
// Use cursor.NewRequest to inject credentials & create the request object before sending
|
||||
models, err := aiService.AvailableModels(context.TODO(), cursor.NewRequest(creds, &aiserverv1.AvailableModelsRequest{
|
||||
IsNightly: true,
|
||||
IncludeLongContextModels: true,
|
||||
}))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("Available models:")
|
||||
for _, model := range models.Msg.ModelNames {
|
||||
fmt.Println(" -", model)
|
||||
}
|
||||
|
||||
model := models.Msg.ModelNames[len(models.Msg.ModelNames)-1]
|
||||
|
||||
fmt.Println("Selected model", model)
|
||||
|
||||
resp, err := aiService.StreamChat(context.TODO(), cursor.NewRequest(creds, &aiserverv1.GetChatRequest{
|
||||
ModelDetails: &aiserverv1.ModelDetails{
|
||||
ModelName: &model,
|
||||
},
|
||||
Conversation: []*aiserverv1.ConversationMessage{
|
||||
{
|
||||
Text: "Hello, who are you?",
|
||||
Type: aiserverv1.ConversationMessage_MESSAGE_TYPE_HUMAN,
|
||||
},
|
||||
},
|
||||
}))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for resp.Receive() {
|
||||
next := resp.Msg()
|
||||
fmt.Printf(next.Text)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
@@ -3,7 +3,7 @@ managed:
|
||||
enabled: true
|
||||
override:
|
||||
- file_option: go_package_prefix
|
||||
value: github.com/everestmz/everestmz.github.io/cursor-reversing/client/cursor/gen
|
||||
value: github.com/everestmz/cursor-rpc/cursor/gen
|
||||
plugins:
|
||||
- local: protoc-gen-go
|
||||
out: gen
|
||||
|
@@ -69076,20 +69076,18 @@ var file_aiserver_v1_aiserver_proto_rawDesc = []byte{
|
||||
0x76, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x65, 0x4e, 0x75,
|
||||
0x6d, 0x62, 0x65, 0x72, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42,
|
||||
0xd1, 0x01, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
|
||||
0xb0, 0x01, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
|
||||
0x2e, 0x76, 0x31, 0x42, 0x0d, 0x41, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x50, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x50, 0x01, 0x5a, 0x62, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x65, 0x76, 0x65, 0x72, 0x65, 0x73, 0x74, 0x6d, 0x7a, 0x2f, 0x65, 0x76, 0x65, 0x72, 0x65,
|
||||
0x73, 0x74, 0x6d, 0x7a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x69, 0x6f, 0x2f, 0x63,
|
||||
0x75, 0x72, 0x73, 0x6f, 0x72, 0x2d, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6e, 0x67, 0x2f,
|
||||
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x2f, 0x67, 0x65,
|
||||
0x6e, 0x2f, 0x61, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x69,
|
||||
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02,
|
||||
0x0b, 0x41, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0b, 0x41,
|
||||
0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x17, 0x41, 0x69, 0x73,
|
||||
0x65, 0x72, 0x76, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61,
|
||||
0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0c, 0x41, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a,
|
||||
0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x74, 0x6f, 0x50, 0x01, 0x5a, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||
0x2f, 0x65, 0x76, 0x65, 0x72, 0x65, 0x73, 0x74, 0x6d, 0x7a, 0x2f, 0x63, 0x75, 0x72, 0x73, 0x6f,
|
||||
0x72, 0x2d, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x2f, 0x67, 0x65, 0x6e,
|
||||
0x2f, 0x61, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x69, 0x73,
|
||||
0x65, 0x72, 0x76, 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0b,
|
||||
0x41, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0b, 0x41, 0x69,
|
||||
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x17, 0x41, 0x69, 0x73, 0x65,
|
||||
0x72, 0x76, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64,
|
||||
0x61, 0x74, 0x61, 0xea, 0x02, 0x0c, 0x41, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x3a,
|
||||
0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
@@ -8,7 +8,7 @@ import (
|
||||
connect "connectrpc.com/connect"
|
||||
context "context"
|
||||
errors "errors"
|
||||
v1 "github.com/everestmz/everestmz.github.io/cursor-reversing/client/cursor/gen/aiserver/v1"
|
||||
v1 "github.com/everestmz/cursor-rpc/cursor/gen/aiserver/v1"
|
||||
http "net/http"
|
||||
strings "strings"
|
||||
)
|
||||
|
5
go.mod
5
go.mod
@@ -1,3 +1,8 @@
|
||||
module github.com/everestmz/cursor-rpc
|
||||
|
||||
go 1.21.0
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.17.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
)
|
||||
|
4
go.sum
Normal file
4
go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk=
|
||||
connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
Reference in New Issue
Block a user