first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,474 @@
package harness
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
"github.com/bytedance/sonic"
)
// Harness defines a coding assistant CLI that Bifrost can launch and manage.
type Harness struct {
ID string
Label string
Binary string
InstallPkg string
VersionArgs []string
BasePath string
BaseURLEnv string
APIKeyEnv string
AuthTokenEnv string
ModelEnv string
SupportsMCP bool
SupportsWorktree bool
RunArgsForMod func(model string) []string
WorktreeArgs func(name string) []string
// PreLaunch is called before launching the harness binary. It can write
// config files and return extra environment variables to inject. The
// returned cleanup function is deferred after the process exits.
PreLaunch func(baseURL, apiKey, model string) (extraEnv []string, cleanup func(), err error)
// WriteNativeConfig persists the bifrost connection settings into the
// harness CLI's own config file so the same configuration is available
// when users launch the CLI directly outside bifrost.
WriteNativeConfig func(baseURL, apiKey, model string) error
// NativeConfigPath is the human-readable path to the file that
// WriteNativeConfig modifies (e.g. "~/.claude/settings.json").
// Used to inform the user in the confirmation prompt.
NativeConfigPath string
}
var all = map[string]Harness{
"claude": {
ID: "claude",
Label: "Claude Code",
Binary: "claude",
InstallPkg: "@anthropic-ai/claude-code",
VersionArgs: []string{"--version"},
BasePath: "/anthropic",
BaseURLEnv: "ANTHROPIC_BASE_URL",
APIKeyEnv: "ANTHROPIC_API_KEY",
AuthTokenEnv: "ANTHROPIC_AUTH_TOKEN",
SupportsMCP: true,
SupportsWorktree: true,
RunArgsForMod: func(model string) []string {
if strings.TrimSpace(model) == "" {
return nil
}
return []string{"--model", model}
},
WorktreeArgs: func(name string) []string {
name = strings.TrimSpace(name)
if name == "" {
return []string{"--worktree"}
}
return []string{"--worktree", name}
},
PreLaunch: claudePreLaunch,
WriteNativeConfig: claudeWriteNativeConfig,
NativeConfigPath: "~/.claude/settings.json",
},
"codex": {
ID: "codex",
Label: "Codex CLI",
Binary: "codex",
InstallPkg: "@openai/codex",
VersionArgs: []string{
"--version",
},
BasePath: "/openai",
BaseURLEnv: "OPENAI_BASE_URL",
APIKeyEnv: "OPENAI_API_KEY",
ModelEnv: "OPENAI_MODEL",
RunArgsForMod: func(model string) []string {
if strings.TrimSpace(model) == "" {
return nil
}
return []string{"--model", model}
},
},
"gemini": {
ID: "gemini",
Label: "Gemini CLI",
Binary: "gemini",
InstallPkg: "@google/gemini-cli",
VersionArgs: []string{
"--version",
},
BasePath: "/genai",
BaseURLEnv: "GOOGLE_GEMINI_BASE_URL",
APIKeyEnv: "GEMINI_API_KEY",
ModelEnv: "GEMINI_MODEL",
RunArgsForMod: func(model string) []string {
if strings.TrimSpace(model) == "" {
return nil
}
return []string{"--model", model}
},
},
"opencode": {
ID: "opencode",
Label: "Opencode",
Binary: "opencode",
InstallPkg: "opencode-ai",
VersionArgs: []string{
"--version",
},
BasePath: "/openai",
BaseURLEnv: "OPENAI_BASE_URL",
APIKeyEnv: "OPENAI_API_KEY",
RunArgsForMod: func(model string) []string {
if strings.TrimSpace(model) == "" {
return nil
}
return []string{"--model", opencodeModelRef(model)}
},
PreLaunch: opencodePreLaunch,
},
}
// Get returns the harness with the given ID and whether it exists.
func Get(id string) (Harness, bool) {
h, ok := all[id]
return h, ok
}
// IDs returns the sorted list of all registered harness IDs.
func IDs() []string {
ids := make([]string, 0, len(all))
for id := range all {
ids = append(ids, id)
}
sort.Strings(ids)
return ids
}
// Labels returns display labels for all harnesses in the format "Label (id)".
func Labels() []string {
ids := IDs()
out := make([]string, 0, len(ids))
for _, id := range ids {
out = append(out, fmt.Sprintf("%s (%s)", all[id].Label, id))
}
return out
}
// ParseChoice extracts the harness ID from a label string like "Label (id)".
func ParseChoice(raw string) string {
raw = strings.TrimSpace(raw)
if i := strings.LastIndex(raw, "("); i >= 0 && strings.HasSuffix(raw, ")") {
return strings.TrimSuffix(raw[i+1:], ")")
}
return raw
}
// DetectVersion runs the harness binary with its version flag and returns the version string.
func DetectVersion(h Harness) string {
if _, err := exec.LookPath(h.Binary); err != nil {
return "not-installed"
}
args := h.VersionArgs
if len(args) == 0 {
args = []string{"--version"}
}
ctx, cancel := context.WithTimeout(context.Background(), 1200*time.Millisecond)
defer cancel()
out, err := exec.CommandContext(ctx, h.Binary, args...).CombinedOutput()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return "timeout"
}
return "unknown"
}
s := strings.TrimSpace(string(out))
if s == "" {
return "unknown"
}
if i := strings.IndexByte(s, '\n'); i >= 0 {
s = s[:i]
}
return s
}
// opencodePreLaunch writes temporary OpenCode config files when Bifrost needs
// to override runtime model/provider settings and/or supply an adaptive TUI
// theme. The returned cleanup removes any generated temp files after exit.
func opencodePreLaunch(baseURL, apiKey, model string) ([]string, func(), error) {
var env []string
var cleanupFns []func()
tuiEnv, tuiCleanup, err := opencodeTUIPreLaunch()
if err != nil {
return nil, nil, err
}
env = append(env, tuiEnv...)
if tuiCleanup != nil {
cleanupFns = append(cleanupFns, tuiCleanup)
}
model = strings.TrimSpace(model)
if model == "" {
return env, combineCleanup(cleanupFns), nil
}
modelRef := opencodeModelRef(model)
runtimeCfg := fmt.Sprintf(`{
"$schema": "https://opencode.ai/config.json",
"model": %q,
"provider": {
"bifrost": {
"npm": "@ai-sdk/openai-compatible",
"name": "Bifrost",
"options": {
"baseURL": %q,
"apiKey": %q
},
"models": {
%q: {
"name": %q
}
}
}
}
}`, modelRef, strings.TrimSpace(baseURL), strings.TrimSpace(apiKey), model, model)
f, err := os.CreateTemp("", "bifrost-opencode-*.json")
if err != nil {
return nil, nil, fmt.Errorf("create opencode config: %w", err)
}
if _, err := f.WriteString(runtimeCfg); err != nil {
f.Close()
os.Remove(f.Name())
return nil, nil, fmt.Errorf("write opencode config: %w", err)
}
if err := f.Close(); err != nil {
os.Remove(f.Name())
return nil, nil, fmt.Errorf("close opencode config: %w", err)
}
env = append(env, "OPENCODE_CONFIG="+f.Name())
cleanupFns = append(cleanupFns, func() { os.Remove(f.Name()) })
return env, combineCleanup(cleanupFns), nil
}
// opencodeModelRef returns the Opencode model reference.
func opencodeModelRef(model string) string {
return "bifrost/" + strings.TrimSpace(model)
}
// opencodeTUIPreLaunch loads the Opencode TUI config from the user's home directory.
func opencodeTUIPreLaunch() ([]string, func(), error) {
path, err := opencodeTUIConfigPath()
if err != nil {
return nil, nil, fmt.Errorf("resolve opencode tui config: %w", err)
}
cfg, hasTheme, err := loadOpencodeTUIConfig(path)
if err != nil {
return nil, nil, err
}
if hasTheme {
return nil, nil, nil
}
if cfg == nil {
cfg = map[string]any{}
}
cfg["theme"] = "system"
b, err := sonic.MarshalIndent(cfg, "", " ")
if err != nil {
return nil, nil, fmt.Errorf("marshal opencode tui config: %w", err)
}
f, err := os.CreateTemp("", "bifrost-opencode-tui-*.json")
if err != nil {
return nil, nil, fmt.Errorf("create opencode tui config: %w", err)
}
if _, err := f.Write(b); err != nil {
f.Close()
os.Remove(f.Name())
return nil, nil, fmt.Errorf("write opencode tui config: %w", err)
}
if err := f.Close(); err != nil {
os.Remove(f.Name())
return nil, nil, fmt.Errorf("close opencode tui config: %w", err)
}
return []string{"OPENCODE_TUI_CONFIG=" + f.Name()}, func() { os.Remove(f.Name()) }, nil
}
// opencodeTUIConfigPath returns the path to the Opencode TUI config.
func opencodeTUIConfigPath() (string, error) {
if xdg := strings.TrimSpace(os.Getenv("XDG_CONFIG_HOME")); xdg != "" {
return filepath.Join(xdg, "opencode", "tui.json"), nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".config", "opencode", "tui.json"), nil
}
// loadOpencodeTUIConfig loads the Opencode TUI config from the given path.
func loadOpencodeTUIConfig(path string) (map[string]any, bool, error) {
b, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, false, nil
}
return nil, false, fmt.Errorf("read opencode tui config: %w", err)
}
normalized := normalizeJSONC(b)
if len(bytes.TrimSpace(normalized)) == 0 {
return map[string]any{}, false, nil
}
var cfg map[string]any
if err := sonic.Unmarshal(normalized, &cfg); err != nil {
return nil, false, fmt.Errorf("parse opencode tui config: %w", err)
}
theme, ok := cfg["theme"]
return cfg, ok && strings.TrimSpace(fmt.Sprint(theme)) != "", nil
}
// combineCleanup combines multiple cleanup functions into a single function.
func combineCleanup(cleanups []func()) func() {
return func() {
for i := len(cleanups) - 1; i >= 0; i-- {
if cleanups[i] != nil {
cleanups[i]()
}
}
}
}
// normalizeJSONC removes trailing commas and comments from JSONC data.
func normalizeJSONC(data []byte) []byte {
return stripTrailingCommas(stripJSONComments(data))
}
// stripJSONComments removes comments from JSONC data.
func stripJSONComments(data []byte) []byte {
out := make([]byte, 0, len(data))
inString := false
escape := false
inLineComment := false
inBlockComment := false
for i := 0; i < len(data); i++ {
ch := data[i]
if inLineComment {
if ch == '\n' {
inLineComment = false
out = append(out, ch)
}
continue
}
if inBlockComment {
if ch == '*' && i+1 < len(data) && data[i+1] == '/' {
inBlockComment = false
i++
}
continue
}
if inString {
out = append(out, ch)
if escape {
escape = false
continue
}
if ch == '\\' {
escape = true
} else if ch == '"' {
inString = false
}
continue
}
if ch == '"' {
inString = true
out = append(out, ch)
continue
}
if ch == '/' && i+1 < len(data) {
switch data[i+1] {
case '/':
inLineComment = true
i++
continue
case '*':
inBlockComment = true
i++
continue
}
}
out = append(out, ch)
}
return out
}
func stripTrailingCommas(data []byte) []byte {
out := make([]byte, 0, len(data))
inString := false
escape := false
for i := 0; i < len(data); i++ {
ch := data[i]
if inString {
out = append(out, ch)
if escape {
escape = false
continue
}
if ch == '\\' {
escape = true
} else if ch == '"' {
inString = false
}
continue
}
if ch == '"' {
inString = true
out = append(out, ch)
continue
}
if ch == ',' {
j := i + 1
for j < len(data) {
switch data[j] {
case ' ', '\t', '\r', '\n':
j++
continue
case '}', ']':
ch = 0
}
break
}
if ch == 0 {
continue
}
}
out = append(out, ch)
}
return out
}

View File

@@ -0,0 +1,246 @@
package harness
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/bytedance/sonic"
)
func TestClaudePreLaunchPinsSelectedModelAcrossClaudeTiers(t *testing.T) {
t.Parallel()
env, cleanup, err := claudePreLaunch("https://example.com/anthropic", "test-key", "openai/gpt-5")
if err != nil {
t.Fatalf("claudePreLaunch() error = %v", err)
}
defer cleanup()
for _, want := range []string{
"CLAUDE_CODE_SIMPLE=1",
"ANTHROPIC_DEFAULT_SONNET_MODEL=openai/gpt-5",
"ANTHROPIC_DEFAULT_OPUS_MODEL=openai/gpt-5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL=openai/gpt-5",
} {
parts := strings.SplitN(want, "=", 2)
if got := envValue(env, parts[0]); got != parts[1] {
t.Fatalf("env[%q] = %q, want %q", parts[0], got, parts[1])
}
}
if got := envValue(env, "ANTHROPIC_MODEL"); got != "" {
t.Fatalf("did not expect ANTHROPIC_MODEL in env, got %#v", env)
}
}
func TestClaudeWriteNativeConfigPinsTierDefaults(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
settingsDir := filepath.Join(home, ".claude")
if err := os.MkdirAll(settingsDir, 0o755); err != nil {
t.Fatalf("mkdir settings dir: %v", err)
}
settingsPath := filepath.Join(settingsDir, "settings.json")
initial := `{"env":{"EXISTING":"keep","ANTHROPIC_MODEL":"stale-model"}}`
if err := os.WriteFile(settingsPath, []byte(initial), 0o600); err != nil {
t.Fatalf("write initial settings: %v", err)
}
if err := claudeWriteNativeConfig("https://example.com/anthropic", "test-key", "openai/gpt-5"); err != nil {
t.Fatalf("claudeWriteNativeConfig() error = %v", err)
}
b, err := os.ReadFile(settingsPath)
if err != nil {
t.Fatalf("read settings: %v", err)
}
var settings map[string]any
if err := sonic.Unmarshal(b, &settings); err != nil {
t.Fatalf("unmarshal settings: %v", err)
}
envRaw, ok := settings["env"]
if !ok {
t.Fatalf("expected env map in settings, got %#v", settings)
}
envMap, ok := envRaw.(map[string]any)
if !ok {
t.Fatalf("env map type = %T, want map[string]any", envRaw)
}
for key, want := range map[string]string{
"EXISTING": "keep",
"ANTHROPIC_BASE_URL": "https://example.com/anthropic",
"ANTHROPIC_API_KEY": "test-key",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "openai/gpt-5",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "openai/gpt-5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "openai/gpt-5",
} {
if got, _ := envMap[key].(string); got != want {
t.Fatalf("env[%q] = %q, want %q", key, got, want)
}
}
if _, ok := envMap["ANTHROPIC_MODEL"]; ok {
t.Fatalf("did not expect legacy ANTHROPIC_MODEL in settings env: %#v", envMap)
}
}
func TestOpencodeModelRef(t *testing.T) {
t.Parallel()
if got := opencodeModelRef("gpt-4.1"); got != "bifrost/gpt-4.1" {
t.Fatalf("opencodeModelRef() = %q, want %q", got, "bifrost/gpt-4.1")
}
}
func TestOpencodePreLaunchWritesCustomProviderConfig(t *testing.T) {
xdg := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", xdg)
env, cleanup, err := opencodePreLaunch("https://example.com/openai", "test-key", "gpt-4.1")
if err != nil {
t.Fatalf("opencodePreLaunch() error = %v", err)
}
defer cleanup()
if len(env) != 2 {
t.Fatalf("unexpected env returned: %#v", env)
}
configPath := envValue(env, "OPENCODE_CONFIG")
if configPath == "" {
t.Fatalf("expected OPENCODE_CONFIG in env, got %#v", env)
}
b, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("read generated config: %v", err)
}
cfg := string(b)
for _, want := range []string{
`"model": "bifrost/gpt-4.1"`,
`"bifrost": {`,
`"npm": "@ai-sdk/openai-compatible"`,
`"baseURL": "https://example.com/openai"`,
`"apiKey": "test-key"`,
`"gpt-4.1": {`,
} {
if !strings.Contains(cfg, want) {
t.Fatalf("expected generated config to contain %q, got %s", want, cfg)
}
}
tuiPath := envValue(env, "OPENCODE_TUI_CONFIG")
if tuiPath == "" {
t.Fatalf("expected OPENCODE_TUI_CONFIG in env, got %#v", env)
}
tuiCfg, err := os.ReadFile(tuiPath)
if err != nil {
t.Fatalf("read generated tui config: %v", err)
}
if !strings.Contains(string(tuiCfg), `"theme": "system"`) {
t.Fatalf("expected generated tui config to set system theme, got %s", string(tuiCfg))
}
}
func TestOpencodePreLaunchPreservesExistingTheme(t *testing.T) {
xdg := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", xdg)
tuiPath := filepath.Join(xdg, "opencode", "tui.json")
if err := os.MkdirAll(filepath.Dir(tuiPath), 0o755); err != nil {
t.Fatalf("mkdir tui dir: %v", err)
}
if err := os.WriteFile(tuiPath, []byte("{\n \"theme\": \"light\"\n}\n"), 0o600); err != nil {
t.Fatalf("write tui config: %v", err)
}
env, cleanup, err := opencodePreLaunch("https://example.com/openai", "test-key", "gpt-4.1")
if err != nil {
t.Fatalf("opencodePreLaunch() error = %v", err)
}
defer cleanup()
if got := envValue(env, "OPENCODE_TUI_CONFIG"); got != "" {
t.Fatalf("did not expect OPENCODE_TUI_CONFIG override when user theme exists, got %#v", env)
}
if got := envValue(env, "OPENCODE_CONFIG"); got == "" {
t.Fatalf("expected OPENCODE_CONFIG to remain present, got %#v", env)
}
}
func TestOpencodePreLaunchAddsSystemThemeWithoutModel(t *testing.T) {
xdg := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", xdg)
env, cleanup, err := opencodePreLaunch("https://example.com/openai", "test-key", "")
if err != nil {
t.Fatalf("opencodePreLaunch() error = %v", err)
}
tuiPath := envValue(env, "OPENCODE_TUI_CONFIG")
if tuiPath == "" {
t.Fatalf("expected OPENCODE_TUI_CONFIG in env, got %#v", env)
}
if got := envValue(env, "OPENCODE_CONFIG"); got != "" {
t.Fatalf("did not expect OPENCODE_CONFIG without a model, got %#v", env)
}
if _, err := os.Stat(tuiPath); err != nil {
t.Fatalf("expected generated tui config to exist: %v", err)
}
cleanup()
if _, err := os.Stat(tuiPath); !os.IsNotExist(err) {
t.Fatalf("expected generated tui config to be removed after cleanup, stat err=%v", err)
}
}
func TestLoadOpencodeTUIConfigSupportsJSONC(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "tui.json")
content := "{\n // keep my choice\n \"theme\": \"light\",\n \"foo\": true,\n}\n"
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("write tui config: %v", err)
}
cfg, hasTheme, err := loadOpencodeTUIConfig(path)
if err != nil {
t.Fatalf("loadOpencodeTUIConfig() error = %v", err)
}
if !hasTheme {
t.Fatal("expected theme to be detected")
}
if cfg["theme"] != "light" {
t.Fatalf("cfg[theme] = %#v, want %q", cfg["theme"], "light")
}
}
func TestOpencodeTUIConfigPathPrefersXDG(t *testing.T) {
xdg := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", xdg)
got, err := opencodeTUIConfigPath()
if err != nil {
t.Fatalf("opencodeTUIConfigPath() error = %v", err)
}
want := filepath.Join(xdg, "opencode", "tui.json")
if got != want {
t.Fatalf("opencodeTUIConfigPath() = %q, want %q", got, want)
}
}
func envValue(env []string, key string) string {
prefix := key + "="
for _, entry := range env {
if strings.HasPrefix(entry, prefix) {
return strings.TrimPrefix(entry, prefix)
}
}
return ""
}

View File

@@ -0,0 +1,98 @@
package harness
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/bytedance/sonic"
"github.com/maximhq/bifrost/cli/internal/config"
)
// claudePreLaunch forces Claude Code into its simpler terminal mode when
// launched inside Bifrost's tab multiplexer. This avoids Claude-specific
// full-screen terminal behavior that doesn't restore reliably across tab swaps.
func claudePreLaunch(baseURL, apiKey, model string) ([]string, func(), error) {
env := []string{"CLAUDE_CODE_SIMPLE=1"}
if model = strings.TrimSpace(model); model != "" {
env = append(env, claudeTierModelEnv(model)...)
}
return env, func() {}, nil
}
// claudeWriteNativeConfig writes the bifrost endpoint, API key, and model
// into Claude Code's settings file (~/.claude/settings.json) so the same
// configuration is available when users launch Claude Code directly.
//
// It merges into the existing file, preserving any user-defined settings.
func claudeWriteNativeConfig(baseURL, apiKey, model string) error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("resolve home dir: %w", err)
}
dir := filepath.Join(home, ".claude")
settingsPath := filepath.Join(dir, "settings.json")
// Read existing settings or start fresh
settings := make(map[string]any)
if b, err := os.ReadFile(settingsPath); err == nil {
if err := sonic.Unmarshal(b, &settings); err != nil {
return fmt.Errorf("parse existing claude settings: %w", err)
}
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("read claude settings: %w", err)
}
// Get or create the env map
envRaw, ok := settings["env"]
var envMap map[string]any
if ok {
envMap, ok = envRaw.(map[string]any)
if !ok {
envMap = make(map[string]any)
}
} else {
envMap = make(map[string]any)
}
envMap["ANTHROPIC_BASE_URL"] = baseURL
envMap["ANTHROPIC_API_KEY"] = apiKey
if model = strings.TrimSpace(model); model != "" {
for key, value := range claudeTierModelEnvMap(model) {
envMap[key] = value
}
delete(envMap, "ANTHROPIC_MODEL")
}
settings["env"] = envMap
b, err := sonic.MarshalIndent(settings, "", " ")
if err != nil {
return fmt.Errorf("marshal claude settings: %w", err)
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("create claude config dir: %w", err)
}
return config.WriteAtomic(settingsPath, b, 0o600)
}
func claudeTierModelEnv(model string) []string {
envMap := claudeTierModelEnvMap(model)
return []string{
"ANTHROPIC_DEFAULT_SONNET_MODEL=" + envMap["ANTHROPIC_DEFAULT_SONNET_MODEL"],
"ANTHROPIC_DEFAULT_OPUS_MODEL=" + envMap["ANTHROPIC_DEFAULT_OPUS_MODEL"],
"ANTHROPIC_DEFAULT_HAIKU_MODEL=" + envMap["ANTHROPIC_DEFAULT_HAIKU_MODEL"],
}
}
func claudeTierModelEnvMap(model string) map[string]string {
return map[string]string{
"ANTHROPIC_DEFAULT_SONNET_MODEL": model,
"ANTHROPIC_DEFAULT_OPUS_MODEL": model,
"ANTHROPIC_DEFAULT_HAIKU_MODEL": model,
}
}