123 lines
2.8 KiB
Go
123 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"syscall"
|
|
)
|
|
|
|
type object = map[string]any
|
|
|
|
type rpcError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type event struct {
|
|
ID json.RawMessage `json:"id"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
Result json.RawMessage `json:"result"`
|
|
Error *rpcError `json:"error"`
|
|
}
|
|
|
|
type client struct {
|
|
cmd *exec.Cmd
|
|
input *json.Encoder
|
|
output *json.Decoder
|
|
id int
|
|
queued []event
|
|
}
|
|
|
|
func (b *bridge) start(ctx context.Context) (*client, error) {
|
|
args := []string{"app-server"}
|
|
if b.mcpURL != "" {
|
|
url, err := json.Marshal(b.mcpURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
args = append(args, "-c", "mcp_servers.gateway.url="+string(url), "-c", "mcp_servers.gateway.required=true")
|
|
}
|
|
cmd := exec.CommandContext(ctx, b.binary, args...)
|
|
cmd.Dir = b.dir
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
|
cmd.Cancel = func() error { return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) }
|
|
in, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
_ = in.Close()
|
|
return nil, err
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
_ = in.Close()
|
|
_ = out.Close()
|
|
return nil, err
|
|
}
|
|
c := &client{cmd: cmd, input: json.NewEncoder(in), output: json.NewDecoder(out)}
|
|
if _, err := c.call("initialize", object{"clientInfo": object{"name": "codex_bridge", "version": "0.1.0"}, "capabilities": object{"experimentalApi": true}}); err != nil {
|
|
c.close()
|
|
return nil, err
|
|
}
|
|
if err := c.input.Encode(object{"method": "initialized", "params": object{}}); err != nil {
|
|
c.close()
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (c *client) close() {
|
|
_ = syscall.Kill(-c.cmd.Process.Pid, syscall.SIGKILL)
|
|
_ = c.cmd.Wait()
|
|
}
|
|
|
|
func (c *client) read() (event, error) {
|
|
var e event
|
|
if err := c.output.Decode(&e); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return e, errors.New("codex app-server closed its output")
|
|
}
|
|
return e, err
|
|
}
|
|
if len(e.ID) > 0 && e.Method != "" {
|
|
return e, fmt.Errorf("unsupported Codex client request: %s", e.Method)
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func (c *client) call(method string, params object) (json.RawMessage, error) {
|
|
c.id++
|
|
id := fmt.Sprint(c.id)
|
|
if err := c.input.Encode(object{"id": c.id, "method": method, "params": params}); err != nil {
|
|
return nil, err
|
|
}
|
|
for {
|
|
e, err := c.read()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if string(e.ID) != id {
|
|
c.queued = append(c.queued, e)
|
|
continue
|
|
}
|
|
if e.Error != nil {
|
|
return nil, fmt.Errorf("%s: %s", method, e.Error.Message)
|
|
}
|
|
return e.Result, nil
|
|
}
|
|
}
|
|
|
|
func (c *client) next() (event, error) {
|
|
if len(c.queued) == 0 {
|
|
return c.read()
|
|
}
|
|
e := c.queued[0]
|
|
c.queued = c.queued[1:]
|
|
return e, nil
|
|
}
|