Files
filepuff-mcp/internal/cursor/cursor.go
T
lukaszraczylo 5ad975ee7a V2/token optimization (#11)
* v2.0: token-optimization overhaul

Additive (backward-compatible flags):
- file_read: skeleton mode, strip (imports/license/block_comments),
  compact_line_numbers, 8-char etag with prefix-match compat
- ast_query: format=verbose|compact|location, pagination cursor
- file_search: cluster mode, pagination cursor
- lsp_query (references): compact output

Breaking (v2):
- Preambles removed; opt-in verbose=true restores
- edit_apply: response=count|diff|none, default count
- ping tool removed
- symbol_at/find_definition/find_references merged into lsp_query
- Tool descriptions trimmed -83%, help moved to filepuff://help/<tool>
- Batch file_read dedups by etag

Protocol:
- ResourceLink returned for file_read >64 KiB (force_inline override)
- OnAfterInitialize hook reads capabilities.experimental.filepuff
  for session defaults (default_format, default_max_results,
  default_cluster, compact_refs, line_numbers,
  resource_link_threshold)

* fix: drop --max-total-count from ripgrep args

The flag does not exist in stable ripgrep (confirmed up to 15.1.0 --
"unrecognized flag --max-total-count, similar flags that are
available: --max-count"). Every file_search call failed on hosts with
stock rg. --max-count is per-file, not a drop-in replacement, so rely
on the in-process truncation in parseOutput that was already the
documented safety net.
2026-04-19 19:56:49 +01:00

66 lines
1.8 KiB
Go

// Package cursor implements opaque pagination cursors for MCP tools.
// A cursor encodes an offset into a result stream plus a query hash so stale
// cursors from different queries fail cleanly.
//
// Encoding: base64url(json({"offset":N,"query_hash":"hex"}))
// The query_hash is a hex-encoded sha256 over the deterministic query params.
package cursor
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"sort"
"strings"
json "github.com/goccy/go-json"
)
// payload is the JSON structure inside a cursor.
type payload struct {
Offset int `json:"offset"`
QueryHash string `json:"query_hash"`
}
// Encode creates an opaque cursor string from an offset and query hash.
func Encode(offset int, queryHash string) string {
p := payload{Offset: offset, QueryHash: queryHash}
b, _ := json.Marshal(p)
return base64.RawURLEncoding.EncodeToString(b)
}
// Decode parses a cursor string. Returns offset, queryHash, error.
func Decode(cursor string) (int, string, error) {
b, err := base64.RawURLEncoding.DecodeString(cursor)
if err != nil {
return 0, "", fmt.Errorf("invalid cursor encoding: %w", err)
}
var p payload
if err := json.Unmarshal(b, &p); err != nil {
return 0, "", fmt.Errorf("invalid cursor payload: %w", err)
}
return p.Offset, p.QueryHash, nil
}
// HashParams computes a deterministic query hash from a set of key=value params.
// Keys are sorted before hashing so order doesn't matter.
func HashParams(params map[string]string) string {
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var sb strings.Builder
for _, k := range keys {
sb.WriteString(k)
sb.WriteByte('=')
sb.WriteString(params[k])
sb.WriteByte('\n')
}
sum := sha256.Sum256([]byte(sb.String()))
return hex.EncodeToString(sum[:])
}