Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions internal/backend/codex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package backend

import (
"bufio"
"fmt"
"os/exec"
"strings"
)

// CodexBackend uses the OpenAI Codex CLI tool as the backend.
// It execs `codex -q <prompt>` and streams stdout.
type CodexBackend struct {
binaryPath string
}

// NewCodexBackend creates a backend that delegates to the Codex CLI.
// If binaryPath is empty, it looks for "codex" in PATH.
func NewCodexBackend(binaryPath string) (*CodexBackend, error) {
if binaryPath == "" {
path, err := exec.LookPath("codex")
if err != nil {
return nil, fmt.Errorf("codex CLI not found in PATH: %w", err)
}
binaryPath = path
}
return &CodexBackend{binaryPath: binaryPath}, nil
}

func (b *CodexBackend) Query(messages []Message, model string, onChunk func(text string)) (string, error) {
// Build the prompt: use the last user message as the primary prompt.
// Pass conversation history as context prepended to the prompt.
var prompt string
var history []string

for i, m := range messages {
if i == len(messages)-1 && m.Role == "user" {
prompt = m.Content
} else {
prefix := "User"
if m.Role == "assistant" {
prefix = "Assistant"
}
history = append(history, fmt.Sprintf("%s: %s", prefix, m.Content))
}
}

if prompt == "" {
return "", fmt.Errorf("no user message found")
}

// Codex CLI uses -q (--quiet) for non-interactive mode that streams to stdout,
// and --approval-mode suggest for safe operation (suggest changes, don't auto-apply).
args := []string{"-q", "--approval-mode", "suggest"}

// Pass model if specified.
if model != "" {
args = append(args, "--model", model)
}

// Prepend conversation history to the prompt for context.
fullPrompt := prompt
if len(history) > 0 {
fullPrompt = "Previous conversation:\n" + strings.Join(history, "\n") + "\n\nCurrent request:\n" + prompt
}

args = append(args, fullPrompt)

cmd := exec.Command(b.binaryPath, args...)

stdout, err := cmd.StdoutPipe()
if err != nil {
return "", fmt.Errorf("failed to create stdout pipe: %w", err)
}

if err := cmd.Start(); err != nil {
return "", fmt.Errorf("failed to start codex CLI: %w", err)
}

var fullResponse strings.Builder
scanner := bufio.NewScanner(stdout)
scanner.Split(scanChunks)

for scanner.Scan() {
chunk := scanner.Text()
fullResponse.WriteString(chunk)
if onChunk != nil {
onChunk(chunk)
}
}

if err := cmd.Wait(); err != nil {
// If we already got some output, return it with the error.
if fullResponse.Len() > 0 {
return fullResponse.String(), fmt.Errorf("codex CLI exited with error: %w", err)
}
return "", fmt.Errorf("codex CLI failed: %w", err)
}

return fullResponse.String(), nil
}
6 changes: 6 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ type Config struct {
// CLI backend settings
CLIPath string `toml:"cli_path"` // Path to claude binary (default: auto-detect)

// Codex backend settings
CodexPath string `toml:"codex_path"` // Path to codex binary (default: auto-detect)

// General settings
SocketPath string `toml:"socket_path"`
PIDFile string `toml:"pid_file"`
Expand Down Expand Up @@ -85,6 +88,9 @@ func Load() (Config, error) {
if v := os.Getenv("INLINE_CLI_CLI_PATH"); v != "" {
cfg.CLIPath = v
}
if v := os.Getenv("INLINE_CLI_CODEX_PATH"); v != "" {
cfg.CodexPath = v
}
if v := os.Getenv("INLINE_CLI_MAX_IDLE"); v != "" {
if n, err := strconv.Atoi(v); err == nil {
cfg.MaxSessionIdleMinutes = n
Expand Down
4 changes: 3 additions & 1 deletion internal/daemon/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ func createBackend(cfg config.Config) (backend.Backend, error) {
switch cfg.Backend {
case "cli":
return backend.NewCLIBackend(cfg.CLIPath)
case "codex":
return backend.NewCodexBackend(cfg.CodexPath)
case "acp":
return backend.NewACPBackend()
case "api", "":
return backend.NewAPIBackend(cfg.APIKey, cfg.APIBaseURL)
default:
return nil, fmt.Errorf("unknown backend: %q (supported: api, cli, acp)", cfg.Backend)
return nil, fmt.Errorf("unknown backend: %q (supported: api, cli, codex, acp)", cfg.Backend)
}
}

Expand Down