* feat: add Codex CLI provider for OpenAI subprocess integration Add CodexCliProvider that wraps `codex exec --json` as a subprocess, analogous to the existing ClaudeCliProvider pattern. This enables using OpenAI's Codex CLI tool as a local LLM backend. - CodexCliProvider: subprocess wrapper parsing JSONL event stream - Credential reader for ~/.codex/auth.json with token expiry detection - Factory integration: provider "codex-cli" and auth_method "codex-cli" - Fix tilde expansion in workspace path for CLI providers - 37 unit tests covering parsing, prompt building, credentials, and mocks * fix: add tool call extraction to Codex CLI provider - Extract shared tool call parsing into tool_call_extract.go (extractToolCallsFromText, stripToolCallsFromText, findMatchingBrace) - Both ClaudeCliProvider and CodexCliProvider now share the same tool call extraction logic for PicoClaw-specific tools - Fix cache token accounting: include cached_input_tokens in total - Add 2 new tests for tool call extraction from JSONL events - Update existing tests for corrected token calculations * fix(docker): update Go version to match go.mod requirement Dockerfile used golang:1.24-alpine but go.mod requires go >= 1.25.7. This caused Docker builds to fail on all branches with: "go: go.mod requires go >= 1.25.7 (running go 1.24.13)" Update to golang:1.25-alpine to match the project requirement. * fix: handle codex CLI stderr noise without losing valid stdout Codex writes diagnostic messages to stderr (e.g. rollout errors) which cause non-zero exit codes even when valid JSONL output exists on stdout. Parse stdout first before checking exit code to avoid false errors. * style: fix gofmt formatting and update web search API in tests - Remove trailing whitespace in web.go and base_test.go - Update config_test.go and web_test.go for WebSearchToolOptions API
80 lines
2.2 KiB
Go
80 lines
2.2 KiB
Go
package providers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// CodexCliAuth represents the ~/.codex/auth.json file structure.
|
|
type CodexCliAuth struct {
|
|
Tokens struct {
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
AccountID string `json:"account_id"`
|
|
} `json:"tokens"`
|
|
}
|
|
|
|
// ReadCodexCliCredentials reads OAuth tokens from the Codex CLI's auth.json file.
|
|
// Expiry is estimated as file modification time + 1 hour (same approach as moltbot).
|
|
func ReadCodexCliCredentials() (accessToken, accountID string, expiresAt time.Time, err error) {
|
|
authPath, err := resolveCodexAuthPath()
|
|
if err != nil {
|
|
return "", "", time.Time{}, err
|
|
}
|
|
|
|
data, err := os.ReadFile(authPath)
|
|
if err != nil {
|
|
return "", "", time.Time{}, fmt.Errorf("reading %s: %w", authPath, err)
|
|
}
|
|
|
|
var auth CodexCliAuth
|
|
if err := json.Unmarshal(data, &auth); err != nil {
|
|
return "", "", time.Time{}, fmt.Errorf("parsing %s: %w", authPath, err)
|
|
}
|
|
|
|
if auth.Tokens.AccessToken == "" {
|
|
return "", "", time.Time{}, fmt.Errorf("no access_token in %s", authPath)
|
|
}
|
|
|
|
stat, err := os.Stat(authPath)
|
|
if err != nil {
|
|
expiresAt = time.Now().Add(time.Hour)
|
|
} else {
|
|
expiresAt = stat.ModTime().Add(time.Hour)
|
|
}
|
|
|
|
return auth.Tokens.AccessToken, auth.Tokens.AccountID, expiresAt, nil
|
|
}
|
|
|
|
// CreateCodexCliTokenSource creates a token source that reads from ~/.codex/auth.json.
|
|
// This allows the existing CodexProvider to reuse Codex CLI credentials.
|
|
func CreateCodexCliTokenSource() func() (string, string, error) {
|
|
return func() (string, string, error) {
|
|
token, accountID, expiresAt, err := ReadCodexCliCredentials()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("reading codex cli credentials: %w", err)
|
|
}
|
|
|
|
if time.Now().After(expiresAt) {
|
|
return "", "", fmt.Errorf("codex cli credentials expired (auth.json last modified > 1h ago). Run: codex login")
|
|
}
|
|
|
|
return token, accountID, nil
|
|
}
|
|
}
|
|
|
|
func resolveCodexAuthPath() (string, error) {
|
|
codexHome := os.Getenv("CODEX_HOME")
|
|
if codexHome == "" {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("getting home dir: %w", err)
|
|
}
|
|
codexHome = filepath.Join(home, ".codex")
|
|
}
|
|
return filepath.Join(codexHome, "auth.json"), nil
|
|
}
|