refactor: items 8-10, 12, 13 from code review

- Move parseRetryAfter to backend.go (where it's called)
- Extract paths.RuntimeDir() replacing 3 divergent XDG_RUNTIME_DIR impls
- Rename CodeWhispererBackend → KiroBackend, NewCodeWhisperer → NewKiro
- Export RPCRequest/RPCError/OutputNotification from toolsrv/rpcwire.go
- Factor tools/lsp/cmdutil: Run() and RunCustom() for 7 LSP binaries
This commit is contained in:
Ollie Agent 2026-08-09 11:32:31 +02:00
parent 9c90cb8954
commit 74635b8e00
19 changed files with 381 additions and 457 deletions

View File

@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)
@ -240,3 +241,22 @@ func isContextOverflow(body string) bool {
}
return false
}
// parseRetryAfter parses the Retry-After header value, which may be an integer
// number of seconds or an HTTP-date. Returns zero if the header is absent or
// unparseable.
func parseRetryAfter(header string) time.Duration {
if header == "" {
return 0
}
header = strings.TrimSpace(header)
if secs, err := strconv.Atoi(header); err == nil {
return time.Duration(secs) * time.Second
}
if t, err := http.ParseTime(header); err == nil {
if d := time.Until(t); d > 0 {
return d
}
}
return 0
}

View File

@ -1,12 +1,12 @@
package backend
// CodeWhispererBackend implements Backend for Amazon CodeWhisperer / Kiro.
// KiroBackend implements Backend for the Kiro streaming API.
//
// COVERAGE: Intentionally untested. Reverse-engineered Kiro streaming API
// client requiring a live session to test.
//
//
// Auth is configured via the apiKey parameter to NewCodeWhisperer:
// Auth is configured via the apiKey parameter to NewKiro:
// - Empty string → read from Kiro CLI SQLite database at the default path
// ($XDG_DATA_HOME/kiro-cli/data.sqlite3 on Linux).
// - "sqlite:///path/to/data.sqlite3" → read from the specified SQLite file.
@ -35,7 +35,7 @@ import (
// ── Public constructor ────────────────────────────────────────────────────────
type CodeWhispererBackend struct {
type KiroBackend struct {
baseBackend
endpoint string // overrides auth-derived endpoint if non-empty
extraHeaders map[string]string
@ -49,13 +49,13 @@ type CodeWhispererBackend struct {
modelsCacheAt time.Time
}
// NewCodeWhisperer returns a CodeWhisperer backend. See package doc for apiKey semantics.
func NewCodeWhisperer(apiKey string) (*CodeWhispererBackend, error) {
// NewKiro returns a Kiro backend. See package doc for apiKey semantics.
func NewKiro(apiKey string) (*KiroBackend, error) {
if strings.TrimSpace(apiKey) == "" {
apiKey = kiroDefaultSQLiteKey()
}
authSource, err := newKiroAuthSource(apiKey)
b := &CodeWhispererBackend{
b := &KiroBackend{
baseBackend: baseBackend{name: "kiro"},
authSource: authSource,
authInitErr: err,
@ -65,9 +65,9 @@ func NewCodeWhisperer(apiKey string) (*CodeWhispererBackend, error) {
return b, nil
}
func (b *CodeWhispererBackend) DefaultModel() string { return "auto" }
func (b *KiroBackend) DefaultModel() string { return "auto" }
func (b *CodeWhispererBackend) Models(ctx context.Context) []string {
func (b *KiroBackend) Models(ctx context.Context) []string {
b.modelsMu.Lock()
if b.modelsCache != nil && time.Since(b.modelsCacheAt) < 24*time.Hour {
result := b.modelsCache
@ -92,7 +92,7 @@ func (b *CodeWhispererBackend) Models(ctx context.Context) []string {
return ids
}
func (b *CodeWhispererBackend) fetchModels(ctx context.Context) *kiroListModelsResponse {
func (b *KiroBackend) fetchModels(ctx context.Context) *kiroListModelsResponse {
if b.authInitErr != nil {
return nil
}
@ -113,7 +113,7 @@ func (b *CodeWhispererBackend) fetchModels(ctx context.Context) *kiroListModelsR
return resp
}
func (b *CodeWhispererBackend) ContextLength(ctx context.Context) int {
func (b *KiroBackend) ContextLength(ctx context.Context) int {
if b.ctxLen > 0 {
return b.ctxLen
}
@ -136,7 +136,7 @@ func (b *CodeWhispererBackend) ContextLength(ctx context.Context) int {
// ── Backend interface ─────────────────────────────────────────────────────────
func (b *CodeWhispererBackend) ChatStream(
func (b *KiroBackend) ChatStream(
ctx context.Context,
messages []Message,
tools []Tool,
@ -172,7 +172,7 @@ const kiroMaxThrottleRetries = 5
// Tests can override this.
var kiroThrottleBaseDelay = 5 * time.Second
func (b *CodeWhispererBackend) runStream(ctx context.Context, req *kiroGenerateRequest, ch chan<- StreamEvent) {
func (b *KiroBackend) runStream(ctx context.Context, req *kiroGenerateRequest, ch chan<- StreamEvent) {
var throttleAttempts int
for {
token, err := b.authSource.AccessToken(ctx)
@ -258,14 +258,14 @@ func (b *CodeWhispererBackend) runStream(ctx context.Context, req *kiroGenerateR
}
}
func (b *CodeWhispererBackend) resolveEndpoint(ctx context.Context) (string, error) {
func (b *KiroBackend) resolveEndpoint(ctx context.Context) (string, error) {
if ep := strings.TrimSpace(b.endpoint); ep != "" {
return ep, nil
}
return b.authSource.DefaultEndpoint(ctx)
}
func (b *CodeWhispererBackend) shouldRefresh(err error) bool {
func (b *KiroBackend) shouldRefresh(err error) bool {
if !b.authSource.CanRefresh() {
return false
}

View File

@ -1,6 +1,6 @@
package backend
// Low-level plumbing for the Amazon CodeWhisperer / Kiro backend.
// Low-level plumbing for the Kiro backend.
// COVERAGE: Intentionally untested. See codewhisperer.go.
//
// - Wire types for the GenerateAssistantResponse API

View File

@ -95,7 +95,7 @@ func newBackendFromConfig(which string, cfg configFile) (Backend, error) {
if token == "" {
token = os.Getenv("OLLIE_KIRO_TOKEN")
}
b, err = NewCodeWhisperer(token)
b, err = NewKiro(token)
case "gemini":
key := bc.Key

View File

@ -9,9 +9,7 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
// OpenAIBackend speaks the OpenAI /v1/chat/completions wire format.
@ -549,21 +547,3 @@ func (b *OpenAIBackend) ChatStream(ctx context.Context, messages []Message, tool
return streamRequest(b.client, httpReq, "openai", streamOpenAISSE)
}
// parseRetryAfter parses the Retry-After header value, which may be an integer
// number of seconds or an HTTP-date. Returns zero if the header is absent or
// unparseable.
func parseRetryAfter(header string) time.Duration {
if header == "" {
return 0
}
header = strings.TrimSpace(header)
if secs, err := strconv.Atoi(header); err == nil {
return time.Duration(secs) * time.Second
}
if t, err := http.ParseTime(header); err == nil {
if d := time.Until(t); d > 0 {
return d
}
}
return 0
}

View File

@ -158,30 +158,14 @@ func serveSocket(ctx context.Context, srv *toolsrv.Server, sockPath string) {
wg.Wait()
}
type rpcRequest struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
type rpcResponse struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Result any `json:"result,omitempty"`
Error *rpcError `json:"error,omitempty"`
Error *toolsrv.RPCError `json:"error,omitempty"`
Stream bool `json:"stream,omitempty"`
}
type rpcError struct {
Code int `json:"code"`
Message string `json:"message"`
}
type outputNotif struct {
Data string `json:"data"`
}
type lockedEncoder struct {
mu sync.Mutex
enc *json.Encoder
@ -198,7 +182,7 @@ func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Wri
enc := &lockedEncoder{enc: json.NewEncoder(out)}
for {
var req rpcRequest
var req toolsrv.RPCRequest
if err := dec.Decode(&req); err != nil {
if err == io.EOF || ctx.Err() != nil {
return
@ -206,7 +190,7 @@ func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Wri
enc.Encode(rpcResponse{
JSONRPC: "2.0",
ID: req.ID,
Error: &rpcError{Code: -32700, Message: "parse error: " + err.Error()},
Error: &toolsrv.RPCError{Code: -32700, Message: "parse error: " + err.Error()},
})
continue
}
@ -215,7 +199,7 @@ func serveRPC(ctx context.Context, srv *toolsrv.Server, in io.Reader, out io.Wri
}
}
func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lockedEncoder) {
func handleRPC(ctx context.Context, srv *toolsrv.Server, req toolsrv.RPCRequest, enc *lockedEncoder) {
switch req.Method {
case "list_tools":
tools, err := srv.ListTools()
@ -223,7 +207,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
enc.Encode(rpcResponse{
JSONRPC: "2.0",
ID: req.ID,
Error: &rpcError{Code: -32000, Message: err.Error()},
Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()},
})
} else {
enc.Encode(rpcResponse{
@ -258,7 +242,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
Value string `json:"value"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}})
return
}
srv.SetEnv(params.Key, params.Value)
@ -269,7 +253,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
Dir string `json:"dir"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}})
return
}
srv.SetCWD(params.Dir)
@ -280,7 +264,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
Names []string `json:"names"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}})
return
}
srv.SetAllowTools(params.Names)
@ -299,11 +283,11 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
Signal int `json:"signal"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}})
return
}
if err := srv.SignalDetached(params.PID, syscall.Signal(params.Signal)); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()}})
} else {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
}
@ -313,12 +297,12 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
PID int `json:"pid"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}})
return
}
out, err := srv.GetDetachedOutput(params.PID)
if err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()}})
} else {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: out})
}
@ -328,7 +312,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
PID int `json:"pid"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}})
return
}
ok := srv.DismissDetached(params.PID)
@ -339,11 +323,11 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
Name string `json:"name"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}})
return
}
if err := srv.LoadTool(params.Name); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32000, Message: err.Error()}})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()}})
} else {
res, _ := json.Marshal(map[string]string{"result": "loaded: " + params.Name})
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(res)})
@ -354,7 +338,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
enc.Encode(rpcResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: outputNotif{Data: data},
Result: toolsrv.OutputNotification{Data: data},
Stream: true,
})
})
@ -363,7 +347,7 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
enc.Encode(rpcResponse{
JSONRPC: "2.0",
ID: req.ID,
Error: &rpcError{Code: -32000, Message: err.Error()},
Error: &toolsrv.RPCError{Code: -32000, Message: err.Error()},
})
} else {
enc.Encode(rpcResponse{

View File

@ -84,11 +84,7 @@ func runServer(sockPath string) {
// Server creation deferred until after bypass broker is ready (see below)
// Start bypass broker
xdg := os.Getenv("XDG_RUNTIME_DIR")
if xdg == "" {
xdg = fmt.Sprintf("/run/user/%d", os.Getuid())
}
bypassSocket := filepath.Join(xdg, "ollie", "bypass.sock")
bypassSocket := filepath.Join(paths.RuntimeDir(), "ollie", "bypass.sock")
os.Setenv("OLLIE_BYPASS_SOCKET", bypassSocket) //nolint:errcheck
policyPath := filepath.Join(paths.DataDir(), "bypass-policy.yaml")

View File

@ -1,6 +1,7 @@
package paths
import (
"fmt"
"os"
"path/filepath"
"strings"
@ -16,6 +17,14 @@ func ExpandHome(path string) string {
return path
}
// RuntimeDir returns $XDG_RUNTIME_DIR or the standard fallback /run/user/UID.
func RuntimeDir() string {
if d := os.Getenv("XDG_RUNTIME_DIR"); d != "" {
return d
}
return fmt.Sprintf("/run/user/%d", os.Getuid())
}
// xdgConfigHome returns $XDG_CONFIG_HOME or ~/.config.
func xdgConfigHome() string {
if p := os.Getenv("XDG_CONFIG_HOME"); p != "" {

View File

@ -1,8 +1,9 @@
package lsp
import (
"os"
"path/filepath"
"ollie/paths"
)
// Adapter describes an LSP server for a specific language.
@ -108,11 +109,7 @@ func LanguageID(path string) string {
// SockPath returns the path to the bridge Unix socket.
func SockPath() string {
xdg := os.Getenv("XDG_RUNTIME_DIR")
if xdg == "" {
xdg = "/tmp"
}
return filepath.Join(xdg, "ollie-lsp.sock")
return filepath.Join(paths.RuntimeDir(), "ollie-lsp.sock")
}
// PidPath returns the path to the bridge PID file.

View File

@ -4,69 +4,32 @@ package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"ollie/tools/lsp"
"ollie/tools/lsp/cmdutil"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "--bridge" {
lsp.RunBridge()
return
}
cmdutil.Run("completion", func(result json.RawMessage) {
var list struct {
Items []completionItem `json:"items"`
}
if json.Unmarshal(result, &list) == nil && len(list.Items) > 0 {
for _, item := range list.Items {
printItem(item)
}
return
}
var args struct {
Path string `json:"path"`
Line string `json:"line"`
Col string `json:"col"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
var items []completionItem
if json.Unmarshal(result, &items) == nil {
for _, item := range items {
printItem(item)
}
return
}
file, _ := filepath.Abs(args.Path)
line, _ := strconv.Atoi(args.Line)
col, _ := strconv.Atoi(args.Col)
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "completion",
File: file,
Line: line - 1,
Col: col - 1,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if result == nil || string(result) == "null" {
fmt.Println("(no completions)")
return
}
// CompletionList or []CompletionItem
var list struct {
Items []completionItem `json:"items"`
}
if json.Unmarshal(result, &list) == nil && len(list.Items) > 0 {
for _, item := range list.Items {
printItem(item)
}
return
}
var items []completionItem
if json.Unmarshal(result, &items) == nil {
for _, item := range items {
printItem(item)
}
return
}
fmt.Println("(no completions)")
})
}
func printItem(item completionItem) {

View File

@ -4,91 +4,50 @@ package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"ollie/tools/lsp"
"ollie/tools/lsp/cmdutil"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "--bridge" {
if err := lsp.RunBridge(); err != nil {
fmt.Fprintf(os.Stderr, "bridge: %v\n", err)
os.Exit(1)
}
return
}
var args struct {
Path string `json:"path"`
Line string `json:"line"`
Col string `json:"col"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
file, _ := filepath.Abs(args.Path)
line, _ := strconv.Atoi(args.Line)
col, _ := strconv.Atoi(args.Col)
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "definition",
File: file,
Line: line - 1, // 1-indexed → 0-indexed
Col: col - 1, // 1-indexed → 0-indexed
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if result == nil || string(result) == "null" {
fmt.Println("(no definition found)")
return
}
// Parse location(s)
var locations []struct {
URI string `json:"uri"`
TargetURI string `json:"targetUri"`
Range *rang `json:"range"`
TargetRange *rang `json:"targetRange"`
}
// Try as array first
if err := json.Unmarshal(result, &locations); err != nil {
// Try as single location
var single struct {
cmdutil.Run("definition", func(result json.RawMessage) {
var locations []struct {
URI string `json:"uri"`
TargetURI string `json:"targetUri"`
Range *rang `json:"range"`
TargetRange *rang `json:"targetRange"`
}
if err := json.Unmarshal(result, &single); err != nil {
fmt.Println("(no definition found)")
return
if err := json.Unmarshal(result, &locations); err != nil {
var single struct {
URI string `json:"uri"`
TargetURI string `json:"targetUri"`
Range *rang `json:"range"`
TargetRange *rang `json:"targetRange"`
}
if err := json.Unmarshal(result, &single); err != nil {
fmt.Println("(no definition found)")
return
}
locations = append(locations, single)
}
locations = append(locations, single)
}
for _, loc := range locations {
uri := loc.URI
if uri == "" {
uri = loc.TargetURI
for _, loc := range locations {
uri := loc.URI
if uri == "" {
uri = loc.TargetURI
}
path := lsp.URIToPath(uri)
r := loc.Range
if r == nil {
r = loc.TargetRange
}
if r == nil {
fmt.Println(path)
continue
}
fmt.Printf("%s:%d:%d\n", path, r.Start.Line+1, r.Start.Character+1)
}
path := lsp.URIToPath(uri)
r := loc.Range
if r == nil {
r = loc.TargetRange
}
if r == nil {
fmt.Println(path)
continue
}
fmt.Printf("%s:%d:%d\n", path, r.Start.Line+1, r.Start.Character+1)
}
})
}
type rang struct {

View File

@ -8,50 +8,48 @@ import (
"path/filepath"
"ollie/tools/lsp"
"ollie/tools/lsp/cmdutil"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "--bridge" {
lsp.RunBridge()
return
}
cmdutil.RunCustom(func() {
var args struct {
Path string `json:"path"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
var args struct {
Path string `json:"path"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
file, _ := filepath.Abs(args.Path)
file, _ := filepath.Abs(args.Path)
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "diagnostics",
File: file,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "diagnostics",
File: file,
if result == nil || string(result) == "null" || string(result) == "[]" {
fmt.Println("(no diagnostics)")
return
}
var diags []diagnostic
if err := json.Unmarshal(result, &diags); err != nil {
fmt.Println("(no diagnostics)")
return
}
for _, d := range diags {
sev := severity(d.Severity)
line := d.Range.Start.Line + 1
col := d.Range.Start.Character + 1
fmt.Printf("%s:%d:%d: %s: %s\n", file, line, col, sev, d.Message)
}
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if result == nil || string(result) == "null" || string(result) == "[]" {
fmt.Println("(no diagnostics)")
return
}
var diags []diagnostic
if err := json.Unmarshal(result, &diags); err != nil {
fmt.Println("(no diagnostics)")
return
}
for _, d := range diags {
sev := severity(d.Severity)
line := d.Range.Start.Line + 1
col := d.Range.Start.Character + 1
fmt.Printf("%s:%d:%d: %s: %s\n", file, line, col, sev, d.Message)
}
}
type diagnostic struct {

View File

@ -4,66 +4,29 @@ package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"ollie/tools/lsp"
"ollie/tools/lsp/cmdutil"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "--bridge" {
lsp.RunBridge()
return
}
var args struct {
Path string `json:"path"`
Line string `json:"line"`
Col string `json:"col"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
file, _ := filepath.Abs(args.Path)
line, _ := strconv.Atoi(args.Line)
col, _ := strconv.Atoi(args.Col)
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "hover",
File: file,
Line: line - 1,
Col: col - 1,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if result == nil || string(result) == "null" {
fmt.Println("(no hover information)")
return
}
var hover struct {
Contents struct {
Kind string `json:"kind"`
Value string `json:"value"`
} `json:"contents"`
}
if err := json.Unmarshal(result, &hover); err != nil {
// Try as plain string
var plain struct {
Contents string `json:"contents"`
cmdutil.Run("hover", func(result json.RawMessage) {
var hover struct {
Contents struct {
Kind string `json:"kind"`
Value string `json:"value"`
} `json:"contents"`
}
if json.Unmarshal(result, &plain) == nil {
fmt.Println(plain.Contents)
if err := json.Unmarshal(result, &hover); err != nil {
var plain struct {
Contents string `json:"contents"`
}
if json.Unmarshal(result, &plain) == nil {
fmt.Println(plain.Contents)
return
}
fmt.Println("(no hover information)")
return
}
fmt.Println("(no hover information)")
return
}
fmt.Println(hover.Contents.Value)
fmt.Println(hover.Contents.Value)
})
}

View File

@ -4,66 +4,31 @@ package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"ollie/tools/lsp"
"ollie/tools/lsp/cmdutil"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "--bridge" {
lsp.RunBridge()
return
}
cmdutil.Run("references", func(result json.RawMessage) {
var locations []struct {
URI string `json:"uri"`
Range struct {
Start struct {
Line int `json:"line"`
Character int `json:"character"`
} `json:"start"`
} `json:"range"`
}
if err := json.Unmarshal(result, &locations); err != nil {
fmt.Println("(no references found)")
return
}
var args struct {
Path string `json:"path"`
Line string `json:"line"`
Col string `json:"col"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
file, _ := filepath.Abs(args.Path)
line, _ := strconv.Atoi(args.Line)
col, _ := strconv.Atoi(args.Col)
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "references",
File: file,
Line: line - 1,
Col: col - 1,
fmt.Printf("STATUS=ok MATCH_COUNT=%d\n", len(locations))
for _, loc := range locations {
path := lsp.URIToPath(loc.URI)
fmt.Printf("%s:%d:%d\n", path, loc.Range.Start.Line+1, loc.Range.Start.Character+1)
}
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if result == nil || string(result) == "null" {
fmt.Println("(no references found)")
return
}
var locations []struct {
URI string `json:"uri"`
Range struct {
Start struct {
Line int `json:"line"`
Character int `json:"character"`
} `json:"start"`
} `json:"range"`
}
if err := json.Unmarshal(result, &locations); err != nil {
fmt.Println("(no references found)")
return
}
fmt.Printf("STATUS=ok MATCH_COUNT=%d\n", len(locations))
for _, loc := range locations {
path := lsp.URIToPath(loc.URI)
fmt.Printf("%s:%d:%d\n", path, loc.Range.Start.Line+1, loc.Range.Start.Character+1)
}
}

View File

@ -11,53 +11,50 @@ import (
"strings"
"ollie/tools/lsp"
"ollie/tools/lsp/cmdutil"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "--bridge" {
lsp.RunBridge()
return
}
cmdutil.RunCustom(func() {
var args struct {
Path string `json:"path"`
Line string `json:"line"`
Col string `json:"col"`
NewName string `json:"new_name"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
var args struct {
Path string `json:"path"`
Line string `json:"line"`
Col string `json:"col"`
NewName string `json:"new_name"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
file, _ := filepath.Abs(args.Path)
line, _ := strconv.Atoi(args.Line)
col, _ := strconv.Atoi(args.Col)
file, _ := filepath.Abs(args.Path)
line, _ := strconv.Atoi(args.Line)
col, _ := strconv.Atoi(args.Col)
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "rename",
File: file,
Line: line - 1,
Col: col - 1,
NewName: args.NewName,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "rename",
File: file,
Line: line - 1,
Col: col - 1,
NewName: args.NewName,
if result == nil || string(result) == "null" {
fmt.Println("(rename produced no changes)")
return
}
summary := applyWorkspaceEdit(result)
if summary == "" {
fmt.Println("(rename produced no changes)")
} else {
fmt.Print(summary)
}
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if result == nil || string(result) == "null" {
fmt.Println("(rename produced no changes)")
return
}
// Parse WorkspaceEdit and apply
summary := applyWorkspaceEdit(result)
if summary == "" {
fmt.Println("(rename produced no changes)")
} else {
fmt.Print(summary)
}
}
func applyWorkspaceEdit(raw json.RawMessage) string {
@ -74,7 +71,6 @@ func applyWorkspaceEdit(raw json.RawMessage) string {
return ""
}
// Normalize to path → edits map
changes := make(map[string][]textEdit)
if len(edit.DocumentChanges) > 0 {
for _, dc := range edit.DocumentChanges {
@ -97,7 +93,6 @@ func applyWorkspaceEdit(raw json.RawMessage) string {
}
lines := strings.SplitAfter(string(data), "\n")
// Sort edits reverse (bottom-up) to preserve positions
sort.Slice(edits, func(i, j int) bool {
if edits[i].Range.Start.Line != edits[j].Range.Start.Line {
return edits[i].Range.Start.Line > edits[j].Range.Start.Line

View File

@ -8,61 +8,57 @@ import (
"path/filepath"
"ollie/tools/lsp"
"ollie/tools/lsp/cmdutil"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "--bridge" {
lsp.RunBridge()
return
}
var args struct {
Path string `json:"path"`
Query string `json:"query"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
file, _ := filepath.Abs(args.Path)
workspace := args.Query != ""
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "symbols",
File: file,
Query: args.Query,
Workspace: workspace,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if result == nil || string(result) == "null" {
fmt.Println("(no symbols found)")
return
}
// Try document symbols (hierarchical)
var docSymbols []documentSymbol
if json.Unmarshal(result, &docSymbols) == nil && len(docSymbols) > 0 {
printDocSymbols(docSymbols, 0)
return
}
// Try workspace symbols (flat)
var wsSymbols []workspaceSymbol
if json.Unmarshal(result, &wsSymbols) == nil && len(wsSymbols) > 0 {
for _, s := range wsSymbols {
path := lsp.URIToPath(s.Location.URI)
line := s.Location.Range.Start.Line + 1
fmt.Printf("%s:%d %s [%s]\n", path, line, s.Name, symbolKind(s.Kind))
cmdutil.RunCustom(func() {
var args struct {
Path string `json:"path"`
Query string `json:"query"`
}
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
return
}
fmt.Println("(no symbols found)")
file, _ := filepath.Abs(args.Path)
workspace := args.Query != ""
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: "symbols",
File: file,
Query: args.Query,
Workspace: workspace,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if result == nil || string(result) == "null" {
fmt.Println("(no symbols found)")
return
}
var docSymbols []documentSymbol
if json.Unmarshal(result, &docSymbols) == nil && len(docSymbols) > 0 {
printDocSymbols(docSymbols, 0)
return
}
var wsSymbols []workspaceSymbol
if json.Unmarshal(result, &wsSymbols) == nil && len(wsSymbols) > 0 {
for _, s := range wsSymbols {
path := lsp.URIToPath(s.Location.URI)
line := s.Location.Range.Start.Line + 1
fmt.Printf("%s:%d %s [%s]\n", path, line, s.Name, symbolKind(s.Kind))
}
return
}
fmt.Println("(no symbols found)")
})
}
func printDocSymbols(symbols []documentSymbol, indent int) {

View File

@ -0,0 +1,85 @@
// Package cmdutil provides shared boilerplate for LSP tool command binaries.
package cmdutil
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"ollie/tools/lsp"
)
// PositionArgs are the common args for position-based LSP commands.
type PositionArgs struct {
Path string `json:"path"`
Line string `json:"line"`
Col string `json:"col"`
}
// ResolvePosition converts the string fields to absolute path and 0-indexed ints.
func (a PositionArgs) ResolvePosition() (file string, line, col int) {
file, _ = filepath.Abs(a.Path)
l, _ := strconv.Atoi(a.Line)
c, _ := strconv.Atoi(a.Col)
return file, l - 1, c - 1
}
// Run is the common entry point for position-based LSP commands.
// It handles --bridge, reads PositionArgs from stdin, dispatches the request,
// and passes the result to format. If format is nil, raw JSON is printed.
func Run(method string, format func(json.RawMessage)) {
if len(os.Args) > 1 && os.Args[1] == "--bridge" {
if err := lsp.RunBridge(); err != nil {
fmt.Fprintf(os.Stderr, "bridge: %v\n", err)
os.Exit(1)
}
return
}
var args PositionArgs
if err := json.NewDecoder(os.Stdin).Decode(&args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
file, line, col := args.ResolvePosition()
result, err := lsp.ClientRequest(&lsp.BridgeRequest{
Method: method,
File: file,
Line: line,
Col: col,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if result == nil || string(result) == "null" {
fmt.Printf("(no %s found)\n", method)
return
}
if format != nil {
format(result)
} else {
os.Stdout.Write(result)
fmt.Println()
}
}
// RunCustom handles --bridge and then calls fn for custom argument handling.
// Use for commands that don't follow the simple position-args pattern
// (e.g. diagnostics, rename, symbols).
func RunCustom(fn func()) {
if len(os.Args) > 1 && os.Args[1] == "--bridge" {
if err := lsp.RunBridge(); err != nil {
fmt.Fprintf(os.Stderr, "bridge: %v\n", err)
os.Exit(1)
}
return
}
fn()
}

22
toolsrv/rpcwire.go Normal file
View File

@ -0,0 +1,22 @@
package toolsrv
import "encoding/json"
// RPCRequest is a JSON-RPC 2.0 request.
type RPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
// RPCError is a JSON-RPC 2.0 error object.
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// OutputNotification carries streamed tool output data.
type OutputNotification struct {
Data string `json:"data"`
}

View File

@ -46,7 +46,7 @@ func localDial(ctx context.Context, cwd string, opts ...Option) (*transport, err
}
// Generate a unique socket path.
sockDir := filepath.Join(runtimeDir(), "ollie")
sockDir := filepath.Join(paths.RuntimeDir(), "ollie")
os.MkdirAll(sockDir, 0700)
sockPath := filepath.Join(sockDir, fmt.Sprintf("toolsrv-%d-%d.sock", os.Getpid(), socketSeq.Add(1)))
@ -137,7 +137,7 @@ func sshDial(ctx context.Context, cfg RemoteConfig) (*transport, error) {
}
// Generate socket paths.
sockDir := filepath.Join(runtimeDir(), "ollie")
sockDir := filepath.Join(paths.RuntimeDir(), "ollie")
os.MkdirAll(sockDir, 0700)
localSock := filepath.Join(sockDir, fmt.Sprintf("remote-%s-%d-%d.sock", cfg.SSHTarget, os.Getpid(), socketSeq.Add(1)))
remoteSock := fmt.Sprintf("/tmp/ollie-toolsrv-%d.sock", os.Getpid())
@ -285,11 +285,3 @@ func shellEscape(s string) string {
}
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
}
// runtimeDir returns XDG_RUNTIME_DIR or a fallback.
func runtimeDir() string {
if d := os.Getenv("XDG_RUNTIME_DIR"); d != "" {
return d
}
return filepath.Join(os.TempDir(), fmt.Sprintf("runtime-%d", os.Getuid()))
}