├── test.txt ├── README.md ├── opencode-test ├── pkg ├── cmd │ ├── interface.go │ └── cmd.go ├── config │ └── config.go ├── assert │ └── assert.go └── program │ └── program.go ├── .gitignore ├── opencode-local ├── go.mod ├── go.sum ├── cmd ├── server │ ├── test.js │ └── main.go ├── example │ └── main.go ├── attach │ └── main.go ├── opencode │ └── main.go └── client │ └── main.go └── out /test.txt: -------------------------------------------------------------------------------- 1 | Hello World 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | I don't want PRs 2 | 3 | PLease leave me alone 4 | 5 | -------------------------------------------------------------------------------- /opencode-test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePrimeagen/daydream/HEAD/opencode-test -------------------------------------------------------------------------------- /pkg/cmd/interface.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | type CLIInterface struct { 4 | } 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | opencode-client 2 | opencode-server 3 | long_running_process 4 | 5 | node_modules 6 | 7 | -------------------------------------------------------------------------------- /opencode-local: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | bun run /home/theprimeagen/personal/opencode/packages/opencode/src/index.ts 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module daydream.theprimeagen.com 2 | 3 | go 1.24.4 4 | 5 | require ( 6 | github.com/creack/pty v1.1.24 7 | golang.org/x/term v0.32.0 8 | ) 9 | 10 | require golang.org/x/sys v0.33.0 11 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const SERVER_SOCKET = "/tmp/opencode-server" 4 | 5 | type CLIConfig struct { 6 | Debug bool 7 | } 8 | 9 | var CLIConfigInstance = CLIConfig{ 10 | Debug: false, 11 | } 12 | 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 2 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 3 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 4 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 5 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 6 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 7 | -------------------------------------------------------------------------------- /cmd/server/test.js: -------------------------------------------------------------------------------- 1 | const path = ["lolomo", 0, 0, ["title", "description"]] 2 | const path2 = ["lolomo", 7, 7, ["title", "description"]] 3 | 4 | datastore = { 5 | "lolomo": { 0: { type: "ref", value: ["list", "uuid"]}}, 6 | "list": { 7 | "uuid": { 8 | 0: { type: "ref", value: ["videos", 1234] }, 9 | } 10 | }, 11 | "videos": { 12 | 1234: { 13 | "title": { type: "atom", value: "Live Free, Die Hard"}, 14 | "description": "bruce willy givin it to us", 15 | } 16 | 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /cmd/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | func write_output() { 10 | for i := range 10000 { 11 | fmt.Printf("Hello, world!: %d\n", i) 12 | time.Sleep(time.Second) 13 | } 14 | } 15 | 16 | func read_input() { 17 | data := make([]byte, 1024) 18 | for { 19 | n, err := os.Stdin.Read(data) 20 | if err != nil { 21 | fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err) 22 | return 23 | } 24 | if n == 0 { 25 | continue 26 | } 27 | fmt.Printf("Key pressed: %c (ASCII: %d)\n", data[0], data[0]) 28 | } 29 | } 30 | 31 | func main() { 32 | go write_output() 33 | go read_input() 34 | 35 | select {} 36 | } 37 | 38 | -------------------------------------------------------------------------------- /pkg/assert/assert.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import ( 4 | "log" 5 | "log/slog" 6 | ) 7 | 8 | var assertData map[string]any = map[string]any{} 9 | 10 | func AddAssertData(key string, value any) { 11 | assertData[key] = value 12 | } 13 | 14 | func RemoveAssertData(key string) { 15 | delete(assertData, key) 16 | } 17 | 18 | func runAssert(msg string) { 19 | for k, v := range assertData { 20 | slog.Error("context", "key", k, "value", v) 21 | } 22 | log.Fatal(msg) 23 | } 24 | 25 | // TODO: Think about passing around a context for debugging purposes 26 | func Assert(truth bool, msg string) { 27 | if !truth { 28 | runAssert(msg) 29 | } 30 | } 31 | 32 | func NoError(err error, msg string) { 33 | if err != nil { 34 | slog.Error("NoError#error encountered", "error", err) 35 | runAssert(msg) 36 | } 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /cmd/attach/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | // Open input_pipe for writing 11 | inputPipe, err := os.OpenFile("input_pipe", os.O_WRONLY, 0) 12 | if err != nil { 13 | fmt.Fprintf(os.Stderr, "Error opening input_pipe: %v\n", err) 14 | os.Exit(1) 15 | } 16 | defer inputPipe.Close() 17 | 18 | // Open output_pipe for reading 19 | outputPipe, err := os.OpenFile("output_pipe", os.O_RDONLY, 0) 20 | if err != nil { 21 | fmt.Fprintf(os.Stderr, "Error opening output_pipe: %v\n", err) 22 | os.Exit(1) 23 | } 24 | defer outputPipe.Close() 25 | 26 | // Start a goroutine to read from output_pipe 27 | go func() { 28 | scanner := bufio.NewScanner(outputPipe) 29 | for scanner.Scan() { 30 | fmt.Println("Received:", scanner.Text()) 31 | } 32 | if err := scanner.Err(); err != nil { 33 | fmt.Fprintf(os.Stderr, "Error reading output_pipe: %v\n", err) 34 | } 35 | }() 36 | 37 | // Read user input from terminal and write to input_pipe 38 | scanner := bufio.NewScanner(os.Stdin) 39 | fmt.Println("Enter messages (Ctrl+D or Ctrl+C to quit):") 40 | for scanner.Scan() { 41 | message := scanner.Text() 42 | _, err := fmt.Fprintln(inputPipe, message) 43 | if err != nil { 44 | fmt.Fprintf(os.Stderr, "Error writing to input_pipe: %v\n", err) 45 | os.Exit(1) 46 | } 47 | inputPipe.Sync() // Flush immediately 48 | } 49 | 50 | if err := scanner.Err(); err != nil { 51 | fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err) 52 | os.Exit(1) 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /cmd/opencode/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "daydream.theprimeagen.com/pkg/program" 11 | "golang.org/x/term" 12 | ) 13 | 14 | func main() { 15 | ctx, cancel := context.WithCancel(context.Background()) 16 | defer cancel() 17 | 18 | debug, err := os.Create("/tmp/test.log") 19 | if err != nil { 20 | slog.Error("failed to create debug log", "error", err) 21 | os.Exit(1) 22 | } 23 | defer debug.Close() 24 | 25 | // Set up signal handling for graceful shutdown 26 | sigChan := make(chan os.Signal, 1) 27 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 28 | 29 | prg := program.NewProgram("opencode").WithWriter(program.FnToWriter(func(b []byte) (int, error) { 30 | os.Stdout.Write(b) 31 | debug.Write(b) 32 | return len(b), nil 33 | })) 34 | 35 | // Store terminal state for restoration 36 | var oldState *term.State 37 | if term.IsTerminal(int(os.Stdin.Fd())) { 38 | oldState, err = term.MakeRaw(int(os.Stdin.Fd())) 39 | if err != nil { 40 | slog.Error("failed to make raw terminal", "error", err) 41 | } 42 | } 43 | 44 | // Ensure terminal is restored on exit 45 | defer func() { 46 | if oldState != nil { 47 | term.Restore(int(os.Stdin.Fd()), oldState) 48 | } 49 | }() 50 | 51 | go func() { 52 | err = prg.Run(ctx) 53 | if err != nil { 54 | slog.Error("failed to run program", "error", err) 55 | } 56 | cancel() 57 | }() 58 | 59 | go prg.PassThroughInput(os.Stdin) 60 | 61 | // Wait for signal or context cancellation 62 | select { 63 | case <-sigChan: 64 | cancel() 65 | case <-ctx.Done(): 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io" 5 | "os/exec" 6 | ) 7 | 8 | 9 | type Command struct { 10 | cmd *exec.Cmd 11 | stdin io.Writer 12 | stdout io.Reader 13 | stderr io.Reader 14 | } 15 | 16 | //func NewCommand() (*Command, error) { 17 | // wd, err := os.Getwd() 18 | // if err != nil { 19 | // slog.Error("failed to get working directory, WTF", "error", err) 20 | // return nil, err 21 | // } 22 | // return &Command{ 23 | // Name: wd, 24 | // }, nil 25 | //} 26 | 27 | type reader struct { 28 | cb func([]byte) 29 | } 30 | 31 | func (r *reader) Read(p []byte) (n int, err error) { 32 | r.cb(p) 33 | return len(p), nil 34 | } 35 | 36 | func toReader(cb func(data []byte)) io.Reader { 37 | return &reader{ 38 | cb: cb, 39 | } 40 | } 41 | 42 | type writer struct { 43 | cb func([]byte) 44 | } 45 | 46 | func (w *writer) Write(p []byte) (n int, err error) { 47 | w.cb(p) 48 | return len(p), nil 49 | } 50 | 51 | func toWriter(cb func(data []byte)) io.Writer { 52 | return &writer{ 53 | cb: cb, 54 | } 55 | } 56 | 57 | func NewCommand() (*Command, error) { 58 | cmd := exec.Command("/home/theprimeagen/personal/daydream/long_running_program") 59 | command := &Command{ 60 | cmd: cmd, 61 | } 62 | 63 | cmd.Stdin = toReader(func(data []byte) { 64 | command.stdin.Write(data) 65 | }) 66 | 67 | cmd.Stdout = toWriter(func(data []byte) { 68 | command.stdout.Write(data) 69 | }) 70 | cmd.Stderr = toWriter(func(data []byte) { 71 | command.stderr.Write(data) 72 | }) 73 | 74 | return command, nil 75 | } 76 | 77 | func (c *Command) Start() error { 78 | err := c.cmd.Start() 79 | if err != nil { 80 | return err 81 | } 82 | return nil 83 | } 84 | 85 | func (c *Command) Connect(stdin io.Reader, stdout io.Writer, stderr io.Writer) error { 86 | //todo: what about buffering of stdout? 87 | c.stdin = stdin 88 | c.stdout = stdout 89 | c.stderr = stderr 90 | } 91 | -------------------------------------------------------------------------------- /cmd/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "net" 6 | "os" 7 | 8 | "daydream.theprimeagen.com/pkg/config" 9 | "golang.org/x/term" 10 | ) 11 | 12 | func readInput(conn net.Conn) { 13 | oldState, err := term.MakeRaw(int(os.Stdin.Fd())) 14 | if err != nil { 15 | slog.Error("failed to make raw terminal", "error", err) 16 | return 17 | } 18 | defer term.Restore(int(os.Stdin.Fd()), oldState) // Restore terminal state on exit 19 | 20 | b := make([]byte, 1) 21 | 22 | for { 23 | n, err := os.Stdin.Read(b) 24 | if err != nil { 25 | slog.Error("failed to read from stdin", "error", err) 26 | return 27 | } 28 | if n == 0 { 29 | continue 30 | } 31 | 32 | conn.Write(b[:n]) 33 | 34 | char := b[0] 35 | if char == 3 { 36 | slog.Info("ctrl+c detected, exiting...") 37 | os.Exit(0) 38 | } 39 | 40 | if char == 4 { 41 | slog.Info("ctrl+d detected, exiting...") 42 | os.Exit(0) 43 | } 44 | } 45 | } 46 | 47 | func writeOutput(conn net.Conn) { 48 | for { 49 | buf := make([]byte, 1024) 50 | n, err := conn.Read(buf) 51 | if err != nil { 52 | slog.Error("failed to read from server", "error", err) 53 | os.Exit(0) 54 | } 55 | os.Stdout.Write(buf[:n]) 56 | } 57 | } 58 | 59 | func main() { 60 | conn, err := net.Dial("unix", config.SERVER_SOCKET) 61 | if err != nil { 62 | slog.Error("failed to connect to server", "error", err) 63 | os.Exit(1) 64 | } 65 | 66 | defer conn.Close() 67 | wd, err := os.Getwd() 68 | if err != nil { 69 | slog.Error("failed to get working directory", "error", err) 70 | os.Exit(1) 71 | } 72 | 73 | slog.Info("connected to server", "working directory", wd) 74 | _, err = conn.Write([]byte(wd + "\n")) 75 | if err != nil { 76 | slog.Error("failed to write to server", "error", err) 77 | os.Exit(1) 78 | } 79 | 80 | go readInput(conn) 81 | writeOutput(conn) 82 | } 83 | 84 | -------------------------------------------------------------------------------- /pkg/program/program.go: -------------------------------------------------------------------------------- 1 | package program 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log/slog" 7 | "os" 8 | "os/exec" 9 | "time" 10 | 11 | "daydream.theprimeagen.com/pkg/assert" 12 | "github.com/creack/pty" 13 | "golang.org/x/sys/unix" 14 | "golang.org/x/term" 15 | ) 16 | 17 | type FnWriter struct { 18 | w func(b []byte) (int, error) 19 | } 20 | 21 | func (f *FnWriter) Write(b []byte) (int, error) { 22 | return f.w(b) 23 | } 24 | 25 | func FnToWriter(w func(b []byte) (int, error)) io.Writer { 26 | return &FnWriter{w: w} 27 | } 28 | 29 | type Program struct { 30 | *os.File 31 | cmd *exec.Cmd 32 | stdin io.WriteCloser 33 | path string 34 | rows int 35 | cols int 36 | writer io.Writer 37 | args []string 38 | } 39 | 40 | func NewProgram(path string) *Program { 41 | width, height, err := term.GetSize(int(os.Stdout.Fd())) 42 | if err != nil { 43 | slog.Error("failed to get terminal size", "error", err) 44 | os.Exit(1) 45 | } 46 | 47 | return &Program{ 48 | path: path, 49 | rows: height, 50 | cols: width, 51 | writer: nil, 52 | cmd: nil, 53 | File: nil, 54 | } 55 | } 56 | 57 | func (a *Program) SendKey(key string) { 58 | for _, k := range key { 59 | a.Write([]byte{byte(k)}) 60 | <-time.After(time.Millisecond * 40) 61 | } 62 | } 63 | 64 | func (a *Program) SendBytes(bytes []byte) { 65 | a.Write(bytes) 66 | } 67 | 68 | func (a *Program) WithArgs(args []string) *Program { 69 | a.args = args 70 | return a 71 | } 72 | 73 | func (a *Program) WithWriter(writer io.Writer) *Program { 74 | if a.writer != nil { 75 | a.writer = io.MultiWriter(a.writer, writer) 76 | } else { 77 | a.writer = writer 78 | } 79 | return a 80 | } 81 | 82 | func setRawMode(f *os.File) error { 83 | fd := int(f.Fd()) 84 | const ioctlReadTermios = unix.TCGETS // Linux 85 | const ioctlWriteTermios = unix.TCSETS // Linux 86 | 87 | termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | // Set raw mode (disable ECHO, ICANON, ISIG, and other processing) 93 | termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON 94 | termios.Oflag &^= unix.OPOST 95 | termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN 96 | termios.Cflag &^= unix.CSIZE | unix.PARENB 97 | termios.Cflag |= unix.CS8 98 | 99 | // Set VMIN and VTIME for non-blocking reads 100 | termios.Cc[unix.VMIN] = 1 101 | termios.Cc[unix.VTIME] = 0 102 | 103 | return unix.IoctlSetTermios(fd, ioctlWriteTermios, termios) 104 | } 105 | 106 | func EchoOff(f *os.File) { 107 | fd := int(f.Fd()) 108 | // const ioctlReadTermios = unix.TIOCGETA // OSX. 109 | const ioctlReadTermios = unix.TCGETS // Linux 110 | // const ioctlWriterTermios = unix.TIOCSETA // OSX. 111 | const ioctlWriteTermios = unix.TCSETS // Linux 112 | 113 | termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios) 114 | if err != nil { 115 | panic(err) 116 | } 117 | 118 | newState := *termios 119 | newState.Lflag &^= unix.ECHO 120 | newState.Lflag |= unix.ICANON | unix.ISIG 121 | newState.Iflag |= unix.ICRNL 122 | if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil { 123 | panic(err) 124 | } 125 | } 126 | 127 | func (a *Program) Run(ctx context.Context) error { 128 | assert.Assert(a.writer != nil, "you must provide a writer before you call run") 129 | assert.Assert(a.File == nil, "you have already started the program") 130 | 131 | cmd := exec.Command(a.path, a.args...) 132 | 133 | a.cmd = cmd 134 | 135 | ptmx, err := pty.Start(cmd) 136 | 137 | err = pty.Setsize(ptmx, &pty.Winsize{ 138 | Rows: uint16(a.rows), 139 | Cols: uint16(a.cols), 140 | }) 141 | 142 | if err != nil { 143 | return err 144 | } 145 | 146 | if err := setRawMode(ptmx); err != nil { 147 | ptmx.Close() 148 | return err 149 | } 150 | 151 | a.File = ptmx 152 | 153 | _, err = io.Copy(a.writer, ptmx) 154 | return err 155 | } 156 | 157 | func (p *Program) PassThroughInput(reader io.Reader) { 158 | b := make([]byte, 3) 159 | 160 | for { 161 | n, err := reader.Read(b) 162 | if err != nil { 163 | slog.Error("failed to read from stdin", "error", err) 164 | return 165 | } 166 | if n == 0 { 167 | continue 168 | } 169 | 170 | p.SendBytes(b[:n]) 171 | } 172 | } 173 | 174 | func (a *Program) Close() error { 175 | err := a.File.Close() 176 | a.File = nil 177 | return err 178 | } 179 | -------------------------------------------------------------------------------- /out: -------------------------------------------------------------------------------- 1 | [?1049h[?1h=[?2004h[?2026$p[?2027$p[?2031$p[?2048$p[?u[?25h[?25l 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | [?1004h[?25h[?25l 1  25 | ~ 26 | ~ 27 | ~ 28 | ~ 29 | ~ 30 | ~ 31 | ~ 32 | ~ 33 | ~ 34 | ~ 35 | ~ 36 | ~ 37 | ~ 38 | ~ 39 | ~ 40 | ~ 41 | ~ 42 | ~ 43 | ~ 44 | ~ 45 | ~ 46 | [No Name] 0,0-1 AllNVIM v0.11.2Nvim is open source and freely distributablehttps://neovim.io/#chattype :help nvim if you are new! type :checkhealth to optimize Nvimtype :q to exit type :help for help type :help news to see changes in v0.11Help poor children in Uganda!type :help iccf for information [?1002h[?1006h 1  47 | ~ 48 | ~ 49 | ~ 50 | ~ 51 | ~ NVIM v0.11.2 52 | ~ 53 | ~ Nvim is open source and freely distributable 54 | ~ https://neovim.io/#chat 55 | ~ 56 | ~ type :help nvim if you are new!  57 | ~ type :checkhealth to optimize Nvim 58 | ~ type :q to exit  59 | ~ type :help for help  60 | ~ 61 | ~ type :help news to see changes in v0.11 62 | ~ 63 | ~ Help poor children in Uganda! 64 | ~ type :help iccf for information  65 | ~ 66 | ~ 67 | ~ 68 | [No Name] 0,0-1 All 69 | [?25h -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "net" 11 | "os" 12 | "os/exec" 13 | "os/signal" 14 | "strings" 15 | 16 | "daydream.theprimeagen.com/pkg/config" 17 | ) 18 | 19 | type CLIServer struct { 20 | clis map[string]*CLIInterface 21 | id int 22 | } 23 | 24 | func NewCLIServer() *CLIServer { 25 | slog.Info("initializing cli server") 26 | return &CLIServer{ 27 | clis: map[string]*CLIInterface{}, 28 | id: 0, 29 | } 30 | } 31 | 32 | func (c *CLIServer) HandleClient(conn net.Conn, id int) error { 33 | data := make([]byte, 1024) 34 | request := "" 35 | slog.Info("waiting for request from client connection", "id", id) 36 | msg, err := conn.Read(data) 37 | if err != nil { 38 | slog.Error("connection faield to read", "error", err, "id", id) 39 | conn.Close() 40 | return err 41 | } 42 | 43 | str := string(data[:msg]) 44 | idx := strings.Index(str, "\n") 45 | slog.Info("received request", "id", id, "string", str, "index", idx) 46 | if idx == -1 { 47 | slog.Error("simple framing protocol failed: no newline on first message", "id", id, "string", str) 48 | return errors.New("simple framing protocol failed: no newline on first message") 49 | } 50 | 51 | request = str[:idx] 52 | 53 | if cli, ok := c.clis[request]; ok { 54 | go cli.AddConnection(conn) 55 | } else { 56 | cli, err := CreateNewOpenCodeSession() 57 | if err != nil { 58 | slog.Error("failed to create new open code session", "error", err, "id", id, "request", request) 59 | conn.Write([]byte("failed to create new open code session")) 60 | conn.Close() 61 | return err 62 | } 63 | c.clis[request] = cli 64 | go cli.Start() 65 | go cli.AddConnection(conn) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (c *CLIServer) Start(ctx context.Context) error { 72 | slog.Info("starting cli server") 73 | listener, err := net.Listen("unix", config.SERVER_SOCKET) 74 | if err != nil { 75 | slog.Error("failed to listen on unix socket", "error", err) 76 | return err 77 | } 78 | 79 | for { 80 | conn, err := listener.Accept() 81 | id := c.id 82 | c.id++ 83 | slog.Info("connection accepted", "error", err, "id", id) 84 | if err != nil { 85 | slog.Error("failed to accept connection", "error", err, "id", id) 86 | continue 87 | } 88 | 89 | slog.Info("new remote connection", "id", id) 90 | go c.HandleClient(conn, id) 91 | } 92 | } 93 | 94 | type CLIInterface struct { 95 | stdin io.WriteCloser 96 | stdout io.ReadCloser 97 | stderr io.ReadCloser 98 | out []io.ReadWriter 99 | cmdStr string 100 | debug *os.File 101 | } 102 | 103 | var debugID = 0 104 | 105 | func NewCLIInterface(cmdStr string, args []string) (*CLIInterface, error) { 106 | cmd := exec.Command(cmdStr, args...) 107 | stdin, err := cmd.StdinPipe() 108 | 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | stdout, err := cmd.StdoutPipe() 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | stderr, err := cmd.StderrPipe() 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | if err := cmd.Start(); err != nil { 124 | return nil, err 125 | } 126 | 127 | inter := &CLIInterface{ 128 | stdout: stdout, 129 | stderr: stderr, 130 | stdin: stdin, 131 | out: []io.ReadWriter{}, 132 | cmdStr: cmdStr, 133 | } 134 | 135 | if config.CLIConfigInstance.Debug { 136 | debugID++ 137 | path := fmt.Sprintf("/tmp/opencode-debug-%d.log", debugID) 138 | 139 | inter.debug, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 140 | if err != nil { 141 | slog.Error("failed to open debug log", "error", err) 142 | return nil, err 143 | } 144 | } 145 | 146 | return inter, nil 147 | } 148 | 149 | func (c *CLIInterface) Start() error { 150 | go func() { 151 | slog.Info("starting cli interface for", "command", c.cmdStr) 152 | data := make([]byte, 1024) 153 | for { 154 | msg, err := c.stdout.Read(data) 155 | if err != nil { 156 | slog.Error("failed to read message", "error", err) 157 | break 158 | } 159 | 160 | out := make([]byte, msg) 161 | copy(out, data[:msg]) 162 | for _, conn := range c.out { 163 | conn.Write(out) 164 | } 165 | 166 | if c.debug != nil { 167 | c.debug.Write(data[:msg]) 168 | } 169 | } 170 | }() 171 | return nil 172 | } 173 | 174 | func (c *CLIInterface) AddConnection(conn net.Conn) { 175 | c.out = append(c.out, conn) 176 | for { 177 | buf := make([]byte, 1024) 178 | n, err := conn.Read(buf) 179 | if err != nil { 180 | slog.Error("failed to read from stdin", "error", err) 181 | return 182 | } 183 | if n == 0 { 184 | continue 185 | } 186 | c.stdin.Write(buf[:n]) 187 | } 188 | } 189 | 190 | func CreateNewOpenCodeSession() (*CLIInterface, error) { 191 | slog.Info("creating new open code session") 192 | return NewCLIInterface("opencode", []string{}) 193 | } 194 | 195 | func main() { 196 | var debug bool 197 | flag.BoolVar(&debug, "debug", false, "enable debug logging") 198 | flag.Parse() 199 | 200 | config.CLIConfigInstance.Debug = debug 201 | 202 | // listen for SIGINT 203 | sigs := make(chan os.Signal, 1) 204 | signal.Notify(sigs, os.Interrupt) 205 | go func() { 206 | <-sigs 207 | slog.Info("received SIGINT, exiting...") 208 | os.Exit(0) 209 | }() 210 | 211 | _ = os.Remove(config.SERVER_SOCKET) 212 | server := NewCLIServer() 213 | if err := server.Start(context.Background()); err != nil { 214 | slog.Error("failed to start server", "error", err) 215 | os.Exit(1) 216 | } 217 | } 218 | --------------------------------------------------------------------------------