Skip to content

Commit

Permalink
✨ Add cursor tools (experiment)
Browse files Browse the repository at this point in the history
  • Loading branch information
wesen committed Jan 24, 2025
1 parent a6bc457 commit 73afb5a
Show file tree
Hide file tree
Showing 6 changed files with 472 additions and 239 deletions.
7 changes: 6 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@
- [x] Implement missing SSE methods
- [x] BUG: killing server doesn't seem to kill hanging connections (when using inspector, for example)

- [ ] Register commands using go introspection, like in pinocchio's tools
- [ ] Allow logging to separate file (to debug claude for example)

- [ ] Register commands using go introspection, like in pinocchio's tools
- [ ] Make it easy to register a struct with multiple tool handlers (say, to keep a single handle to a resource), linked to the session_id
- [ ] Pass the session id to the tool (maybe as part of the context?)o
- [ ] Track crashes in a log file
139 changes: 102 additions & 37 deletions pkg/tools/cursor/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"strings"

"github.com/go-go-golems/go-go-mcp/pkg/protocol"
"github.com/go-go-golems/go-go-mcp/pkg/tools"
Expand All @@ -16,29 +17,43 @@ func RegisterExtractCodeBlocksTool(registry *tools.Registry) error {
schemaJson := `{
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "The filepath to extract code blocks for"
"filepaths": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of file paths or search terms to extract code blocks for. Each term can be a file path (relative or absolute) or a search term. Terms containing spaces should be wrapped in quotes. Example: ['file.go', 'other.go', 'some search term']. Multiple unquoted words are treated as separate search terms."
}
},
"required": ["filepath"]
"required": ["filepaths"]
}`

tool, err := tools.NewToolImpl("cursor-extract-code-blocks", "Get all code blocks for a file", json.RawMessage(schemaJson))
tool, err := tools.NewToolImpl(
"cursor-extract-code-blocks",
"Extract all code blocks that were generated or modified by the AI for multiple files or search terms. Returns a structured YAML output containing code blocks organized by file and conversation. Accepts space-separated terms (use quotes for terms containing spaces) for flexible searching. Example: file.go 'quoted term' other.go",
json.RawMessage(schemaJson))
if err != nil {
return err
}

registry.RegisterToolWithHandler(
tool,
func(ctx context.Context, tool tools.Tool, arguments map[string]interface{}) (*protocol.ToolResult, error) {
filepath, ok := arguments["filepath"].(string)
filepaths, ok := arguments["filepaths"].([]interface{})
if !ok {
return protocol.NewToolResult(
protocol.WithError("filepath argument must be a string"),
protocol.WithError("filepaths argument must be an array of strings"),
), nil
}

// Process each filepath/search term
var searchTerms []string
for _, path := range filepaths {
pathStr := fmt.Sprintf("%v", path)
terms := splitSearchTerms(pathStr)
searchTerms = append(searchTerms, terms...)
}

db, err := sql.Open("sqlite3", defaultDBPath)
if err != nil {
return protocol.NewToolResult(
Expand All @@ -47,26 +62,36 @@ func RegisterExtractCodeBlocksTool(registry *tools.Registry) error {
}
defer db.Close()

query := `
// Build LIKE conditions for each search term
conditions := make([]string, len(searchTerms))
args := make([]interface{}, len(searchTerms))
for i, term := range searchTerms {
conditions[i] = "value LIKE ?"
args[i] = "%" + term + "%"
}

query := fmt.Sprintf(`
SELECT key,
json_extract(value, '$.conversation[1].codeBlocks') as code_blocks
json_extract(value, '$.conversation[1].codeBlocks') as code_blocks,
value as full_value
FROM cursorDiskKV
WHERE value LIKE ?
`
WHERE %s
`, strings.Join(conditions, " OR "))

rows, err := db.QueryContext(ctx, query, "%"+filepath+"%")
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return protocol.NewToolResult(
protocol.WithError(fmt.Sprintf("error executing query: %v", err)),
), nil
}
defer rows.Close()

var results []map[string]interface{}
results := make(map[string][]map[string]interface{})
for rows.Next() {
var key string
var codeBlocks string
if err := rows.Scan(&key, &codeBlocks); err != nil {
var fullValue string
if err := rows.Scan(&key, &codeBlocks, &fullValue); err != nil {
return protocol.NewToolResult(
protocol.WithError(fmt.Sprintf("error scanning row: %v", err)),
), nil
Expand All @@ -77,10 +102,18 @@ func RegisterExtractCodeBlocksTool(registry *tools.Registry) error {
continue // Skip invalid JSON
}

results = append(results, map[string]interface{}{
"key": key,
"code_blocks": blocks,
})
// Match against each search term
for _, term := range searchTerms {
if strings.Contains(fullValue, term) {
if _, ok := results[term]; !ok {
results[term] = []map[string]interface{}{}
}
results[term] = append(results[term], map[string]interface{}{
"key": key,
"code_blocks": blocks,
})
}
}
}

yamlData, err := yaml.Marshal(results)
Expand All @@ -102,29 +135,43 @@ func RegisterTrackFileModificationsTool(registry *tools.Registry) error {
schemaJson := `{
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "The filepath to track modifications for"
"filepaths": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of file paths or search terms to track modifications for. Each term can be a file path (relative or absolute) or a search term. Terms containing spaces should be wrapped in quotes. Example: ['file.go', 'other.go', 'some search term']. Multiple unquoted words are treated as separate search terms."
}
},
"required": ["filepath"]
"required": ["filepaths"]
}`

tool, err := tools.NewToolImpl("cursor-track-file-modifications", "Track changes to a file over time", json.RawMessage(schemaJson))
tool, err := tools.NewToolImpl(
"cursor-track-file-modifications",
"Track and analyze all modifications made to files matching the specified search terms through Cursor AI interactions over time. Returns a chronological history of changes for each matching file or term. Accepts space-separated terms (use quotes for terms containing spaces) for flexible searching. Example: file.go 'quoted term' other.go",
json.RawMessage(schemaJson))
if err != nil {
return err
}

registry.RegisterToolWithHandler(
tool,
func(ctx context.Context, tool tools.Tool, arguments map[string]interface{}) (*protocol.ToolResult, error) {
filepath, ok := arguments["filepath"].(string)
filepaths, ok := arguments["filepaths"].([]interface{})
if !ok {
return protocol.NewToolResult(
protocol.WithError("filepath argument must be a string"),
protocol.WithError("filepaths argument must be an array of strings"),
), nil
}

// Process each filepath/search term
var searchTerms []string
for _, path := range filepaths {
pathStr := fmt.Sprintf("%v", path)
terms := splitSearchTerms(pathStr)
searchTerms = append(searchTerms, terms...)
}

db, err := sql.Open("sqlite3", defaultDBPath)
if err != nil {
return protocol.NewToolResult(
Expand All @@ -133,33 +180,43 @@ func RegisterTrackFileModificationsTool(registry *tools.Registry) error {
}
defer db.Close()

query := `
// Build LIKE conditions for each search term
conditions := make([]string, len(searchTerms))
args := make([]interface{}, len(searchTerms))
for i, term := range searchTerms {
conditions[i] = "value LIKE ?"
args[i] = "%" + term + "%"
}

query := fmt.Sprintf(`
WITH RECURSIVE
file_mods AS (
SELECT key,
json_extract(value, '$.createdAt') as created_at,
json_extract(value, '$.conversation[1].codeBlocks') as code_blocks
json_extract(value, '$.conversation[1].codeBlocks') as code_blocks,
value as full_value
FROM cursorDiskKV
WHERE value LIKE ?
WHERE %s
ORDER BY created_at ASC
)
SELECT * FROM file_mods
`
`, strings.Join(conditions, " OR "))

rows, err := db.QueryContext(ctx, query, "%"+filepath+"%")
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return protocol.NewToolResult(
protocol.WithError(fmt.Sprintf("error executing query: %v", err)),
), nil
}
defer rows.Close()

var results []map[string]interface{}
results := make(map[string][]map[string]interface{})
for rows.Next() {
var key string
var createdAt string
var codeBlocks string
if err := rows.Scan(&key, &createdAt, &codeBlocks); err != nil {
var fullValue string
if err := rows.Scan(&key, &createdAt, &codeBlocks, &fullValue); err != nil {
return protocol.NewToolResult(
protocol.WithError(fmt.Sprintf("error scanning row: %v", err)),
), nil
Expand All @@ -170,11 +227,19 @@ func RegisterTrackFileModificationsTool(registry *tools.Registry) error {
continue // Skip invalid JSON
}

results = append(results, map[string]interface{}{
"key": key,
"created_at": createdAt,
"code_blocks": blocks,
})
// Match against each search term
for _, term := range searchTerms {
if strings.Contains(fullValue, term) {
if _, ok := results[term]; !ok {
results[term] = []map[string]interface{}{}
}
results[term] = append(results[term], map[string]interface{}{
"key": key,
"created_at": createdAt,
"code_blocks": blocks,
})
}
}
}

yamlData, err := yaml.Marshal(results)
Expand Down
Loading

0 comments on commit 73afb5a

Please sign in to comment.