185 lines
4.3 KiB
Go
185 lines
4.3 KiB
Go
package geminiadapter
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
type rpcClient interface {
|
|
Initialize() (initializeResult, error)
|
|
Call(method string, params map[string]any) (map[string]any, error)
|
|
Close() error
|
|
}
|
|
|
|
type initializeResult struct {
|
|
ProtocolVersion int `json:"protocolVersion"`
|
|
AuthMethods []map[string]any `json:"authMethods"`
|
|
AgentCapabilities map[string]any `json:"agentCapabilities"`
|
|
}
|
|
|
|
type stdioRPCClient struct {
|
|
mu sync.Mutex
|
|
command string
|
|
args []string
|
|
env []string
|
|
protocolVersion int
|
|
cmd *exec.Cmd
|
|
stdin io.WriteCloser
|
|
stdout *bufio.Reader
|
|
stderr io.ReadCloser
|
|
nextID atomic.Int64
|
|
initialized bool
|
|
initResult initializeResult
|
|
}
|
|
|
|
func newStdioRPCClient(
|
|
command string,
|
|
args []string,
|
|
env []string,
|
|
protocolVersion int,
|
|
) *stdioRPCClient {
|
|
return &stdioRPCClient{
|
|
command: strings.TrimSpace(command),
|
|
args: append([]string(nil), args...),
|
|
env: append([]string(nil), env...),
|
|
protocolVersion: protocolVersion,
|
|
}
|
|
}
|
|
|
|
func (c *stdioRPCClient) Initialize() (initializeResult, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if err := c.ensureStartedLocked(); err != nil {
|
|
return initializeResult{}, err
|
|
}
|
|
if c.initialized {
|
|
return c.initResult, nil
|
|
}
|
|
result, err := c.callLocked("initialize", map[string]any{
|
|
"protocolVersion": c.protocolVersion,
|
|
"clientInfo": map[string]any{
|
|
"name": "xworkmate-gemini-adapter",
|
|
"version": "0.1.0",
|
|
},
|
|
})
|
|
if err != nil {
|
|
return initializeResult{}, err
|
|
}
|
|
payload, _ := result["result"].(map[string]any)
|
|
data, _ := json.Marshal(payload)
|
|
var parsed initializeResult
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
return initializeResult{}, fmt.Errorf("decode initialize result: %w", err)
|
|
}
|
|
c.initialized = true
|
|
c.initResult = parsed
|
|
return parsed, nil
|
|
}
|
|
|
|
func (c *stdioRPCClient) Call(method string, params map[string]any) (map[string]any, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if err := c.ensureStartedLocked(); err != nil {
|
|
return nil, err
|
|
}
|
|
return c.callLocked(method, params)
|
|
}
|
|
|
|
func (c *stdioRPCClient) Close() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.closeLocked()
|
|
}
|
|
|
|
func (c *stdioRPCClient) ensureStartedLocked() error {
|
|
if c.cmd != nil {
|
|
return nil
|
|
}
|
|
if c.command == "" {
|
|
return fmt.Errorf("gemini command is empty")
|
|
}
|
|
cmd := exec.Command(c.command, c.args...)
|
|
cmd.Env = append(os.Environ(), c.env...)
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
c.cmd = cmd
|
|
c.stdin = stdin
|
|
c.stdout = bufio.NewReader(stdout)
|
|
c.stderr = stderr
|
|
return nil
|
|
}
|
|
|
|
func (c *stdioRPCClient) closeLocked() error {
|
|
var firstErr error
|
|
if c.stdin != nil {
|
|
if err := c.stdin.Close(); err != nil && firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
if c.cmd != nil && c.cmd.Process != nil {
|
|
if err := c.cmd.Process.Kill(); err != nil && firstErr == nil && !strings.Contains(strings.ToLower(err.Error()), "finished") {
|
|
firstErr = err
|
|
}
|
|
_, _ = c.cmd.Process.Wait()
|
|
}
|
|
c.cmd = nil
|
|
c.stdin = nil
|
|
c.stdout = nil
|
|
c.stderr = nil
|
|
c.initialized = false
|
|
c.initResult = initializeResult{}
|
|
return firstErr
|
|
}
|
|
|
|
func (c *stdioRPCClient) callLocked(method string, params map[string]any) (map[string]any, error) {
|
|
requestID := fmt.Sprintf("req-%d", c.nextID.Add(1))
|
|
request := map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": requestID,
|
|
"method": strings.TrimSpace(method),
|
|
"params": params,
|
|
}
|
|
encoded, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := c.stdin.Write(append(encoded, '\n')); err != nil {
|
|
return nil, err
|
|
}
|
|
line, err := c.stdout.ReadBytes('\n')
|
|
if err != nil {
|
|
if stderr, stderrErr := io.ReadAll(c.stderr); stderrErr == nil {
|
|
trimmed := strings.TrimSpace(string(stderr))
|
|
if trimmed != "" {
|
|
return nil, fmt.Errorf("gemini acp read failed: %s", trimmed)
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
var response map[string]any
|
|
if err := json.Unmarshal(line, &response); err != nil {
|
|
return nil, fmt.Errorf("decode gemini acp response: %w", err)
|
|
}
|
|
return response, nil
|
|
}
|