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:
|
extract-schema:
|
||||||
go run cmd/extract
|
go run ./cmd/extract
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
cd cursor && buf 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
|
## 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
|
## 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
|
enabled: true
|
||||||
override:
|
override:
|
||||||
- file_option: go_package_prefix
|
- 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:
|
plugins:
|
||||||
- local: protoc-gen-go
|
- local: protoc-gen-go
|
||||||
out: gen
|
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,
|
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,
|
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,
|
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,
|
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,
|
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, 0x65, 0x76, 0x65, 0x72, 0x65,
|
0x2f, 0x65, 0x76, 0x65, 0x72, 0x65, 0x73, 0x74, 0x6d, 0x7a, 0x2f, 0x63, 0x75, 0x72, 0x73, 0x6f,
|
||||||
0x73, 0x74, 0x6d, 0x7a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x69, 0x6f, 0x2f, 0x63,
|
0x72, 0x2d, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x2f, 0x67, 0x65, 0x6e,
|
||||||
0x75, 0x72, 0x73, 0x6f, 0x72, 0x2d, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6e, 0x67, 0x2f,
|
0x2f, 0x61, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x69, 0x73,
|
||||||
0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x2f, 0x67, 0x65,
|
0x65, 0x72, 0x76, 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x0b,
|
||||||
0x6e, 0x2f, 0x61, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x69,
|
0x41, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0b, 0x41, 0x69,
|
||||||
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02,
|
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x17, 0x41, 0x69, 0x73, 0x65,
|
||||||
0x0b, 0x41, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0b, 0x41,
|
0x72, 0x76, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64,
|
||||||
0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x17, 0x41, 0x69, 0x73,
|
0x61, 0x74, 0x61, 0xea, 0x02, 0x0c, 0x41, 0x69, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x3a,
|
||||||
0x65, 0x72, 0x76, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61,
|
0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
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 (
|
var (
|
||||||
|
@@ -8,7 +8,7 @@ import (
|
|||||||
connect "connectrpc.com/connect"
|
connect "connectrpc.com/connect"
|
||||||
context "context"
|
context "context"
|
||||||
errors "errors"
|
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"
|
http "net/http"
|
||||||
strings "strings"
|
strings "strings"
|
||||||
)
|
)
|
||||||
|
5
go.mod
5
go.mod
@@ -1,3 +1,8 @@
|
|||||||
module github.com/everestmz/cursor-rpc
|
module github.com/everestmz/cursor-rpc
|
||||||
|
|
||||||
go 1.21.0
|
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