3148fdfa92
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
2.7 KiB
Go
102 lines
2.7 KiB
Go
package ai
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// claudeBackend shells out to `claude --print` with the prompt on stdin and the
|
|
// answer on stdout.
|
|
type claudeBackend struct {
|
|
cmd string
|
|
args []string
|
|
}
|
|
|
|
func newClaudeBackend() claudeBackend {
|
|
return claudeBackend{
|
|
cmd: "claude",
|
|
args: []string{"--print", "--tools", "", "--no-session-persistence", "--output-format", "text"},
|
|
}
|
|
}
|
|
|
|
func (b claudeBackend) Name() string { return "claude" }
|
|
|
|
func (b claudeBackend) Run(ctx context.Context, prompt string) (string, error) {
|
|
cmd := exec.CommandContext(ctx, b.cmd, b.args...)
|
|
cmd.Stdin = strings.NewReader(prompt)
|
|
var out, errb bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &errb
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("claude: %w: %s", err, strings.TrimSpace(errb.String()))
|
|
}
|
|
return out.String(), nil
|
|
}
|
|
|
|
// codexBackend shells out to `codex exec` headlessly. codex's stdout includes
|
|
// session preamble, so the final answer is written to a temp file via -o and
|
|
// read back. The --ignore-* and read-only flags stop it from running shell
|
|
// commands driven by local config.
|
|
type codexBackend struct {
|
|
cmd string
|
|
baseArgs []string
|
|
}
|
|
|
|
func newCodexBackend() codexBackend {
|
|
return codexBackend{
|
|
cmd: "codex",
|
|
baseArgs: []string{
|
|
"exec", "--skip-git-repo-check", "--ignore-user-config",
|
|
"--ignore-rules", "-s", "read-only", "-a", "never", "--ephemeral",
|
|
},
|
|
}
|
|
}
|
|
|
|
func (b codexBackend) Name() string { return "codex" }
|
|
|
|
// args builds the full argument list, writing the answer to outfile and reading
|
|
// the prompt from stdin (trailing "-").
|
|
func (b codexBackend) args(outfile string) []string {
|
|
out := append([]string{}, b.baseArgs...)
|
|
return append(out, "-o", outfile, "-")
|
|
}
|
|
|
|
func (b codexBackend) Run(ctx context.Context, prompt string) (string, error) {
|
|
f, err := os.CreateTemp("", "antidrift-codex-*.out")
|
|
if err != nil {
|
|
return "", fmt.Errorf("codex: temp file: %w", err)
|
|
}
|
|
path := f.Name()
|
|
_ = f.Close()
|
|
defer os.Remove(path)
|
|
|
|
cmd := exec.CommandContext(ctx, b.cmd, b.args(path)...)
|
|
cmd.Stdin = strings.NewReader(prompt)
|
|
var errb bytes.Buffer
|
|
cmd.Stderr = &errb // stdout is noisy and intentionally discarded
|
|
if err := cmd.Run(); err != nil {
|
|
return "", fmt.Errorf("codex: %w: %s", err, strings.TrimSpace(errb.String()))
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", fmt.Errorf("codex: read output: %w", err)
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
// NewBackend returns the named backend. "" defaults to "claude".
|
|
func NewBackend(name string) (Backend, error) {
|
|
switch name {
|
|
case "", "claude":
|
|
return newClaudeBackend(), nil
|
|
case "codex":
|
|
return newCodexBackend(), nil
|
|
default:
|
|
return nil, fmt.Errorf("ai: unknown backend %q", name)
|
|
}
|
|
}
|