Files
picoclaw/pkg/providers/codex_cli_credentials.go
Leandro Barbosa e77b0a6755 feat: add Codex CLI provider for subprocess integration (#80)
* 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
2026-02-16 11:40:17 +08:00

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
}