mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-12 04:54:27 +00:00
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.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// 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[:])
|
||||
}
|
||||
Reference in New Issue
Block a user