package qoder import ( "context" "fmt" "log/slog" "os" "os/exec" "path/filepath" "strings" "sync" "github.com/chenhg5/cc-connect/core" ) func init() { core.RegisterAgent("qoder", New) } // Agent drives Qoder CLI using `qodercli -p -f stream-json`. type Agent struct { workDir string model string mode string // "default" | "yolo" sessionEnv []string mu sync.Mutex } func New(opts map[string]any) (core.Agent, error) { workDir, _ := opts["work_dir"].(string) if workDir == "" { workDir = "." } model, _ := opts["model"].(string) mode, _ := opts["mode"].(string) mode = normalizeMode(mode) if _, err := exec.LookPath("qodercli"); err != nil { return nil, fmt.Errorf("qoder: 'qodercli' not found in PATH, install with: curl -fsSL https://qoder.com/install | bash") } return &Agent{ workDir: workDir, model: model, mode: mode, }, nil } func normalizeMode(raw string) string { switch strings.ToLower(strings.TrimSpace(raw)) { case "yolo", "bypass", "dangerously-skip-permissions": return "yolo" default: return "default" } } func (a *Agent) Name() string { return "qoder" } func (a *Agent) CLIBinaryName() string { return "qodercli" } func (a *Agent) CLIDisplayName() string { return "Qoder" } func (a *Agent) SetWorkDir(dir string) { a.mu.Lock() defer a.mu.Unlock() a.workDir = dir slog.Info("qoder: work_dir changed", "work_dir", dir) } func (a *Agent) GetWorkDir() string { a.mu.Lock() defer a.mu.Unlock() return a.workDir } func (a *Agent) SetModel(model string) { a.mu.Lock() defer a.mu.Unlock() a.model = model slog.Info("qoder: model changed", "model", model) } func (a *Agent) GetModel() string { a.mu.Lock() defer a.mu.Unlock() return a.model } func (a *Agent) AvailableModels(_ context.Context) []core.ModelOption { return []core.ModelOption{ {Name: "auto", Desc: "Auto (recommended)"}, {Name: "ultimate", Desc: "Ultimate (most capable)"}, {Name: "performance", Desc: "Performance (balanced)"}, {Name: "efficient", Desc: "Efficient (fast)"}, {Name: "lite", Desc: "Lite (lightweight)"}, } } func (a *Agent) SetSessionEnv(env []string) { a.mu.Lock() defer a.mu.Unlock() a.sessionEnv = env } func (a *Agent) StartSession(ctx context.Context, sessionID string) (core.AgentSession, error) { a.mu.Lock() mode := a.mode model := a.model workDir := a.workDir extraEnv := append([]string{}, a.sessionEnv...) a.mu.Unlock() return newQoderSession(ctx, workDir, model, mode, sessionID, extraEnv) } func (a *Agent) ListSessions(_ context.Context) ([]core.AgentSessionInfo, error) { return nil, nil } func (a *Agent) Stop() error { return nil } // ── ModeSwitcher ───────────────────────────────────────────── func (a *Agent) SetMode(mode string) { a.mu.Lock() defer a.mu.Unlock() a.mode = normalizeMode(mode) slog.Info("qoder: mode changed", "mode", a.mode) } func (a *Agent) GetMode() string { a.mu.Lock() defer a.mu.Unlock() return a.mode } func (a *Agent) PermissionModes() []core.PermissionModeInfo { return []core.PermissionModeInfo{ {Key: "default", Name: "Default", NameZh: "默认", Desc: "Standard permissions", DescZh: "标准权限模式"}, {Key: "yolo", Name: "YOLO", NameZh: "全自动", Desc: "Skip all permission checks", DescZh: "跳过所有权限检查"}, } } // ── SkillProvider ──────────────────────────────────────────── func (a *Agent) SkillDirs() []string { workDir := a.GetWorkDir() absDir, err := filepath.Abs(workDir) if err != nil { absDir = workDir } dirs := []string{filepath.Join(absDir, ".claude", "skills")} if home, err := os.UserHomeDir(); err == nil { dirs = append(dirs, filepath.Join(home, ".claude", "skills")) } return dirs } // ── ContextCompressor ──────────────────────────────────────── func (a *Agent) CompressCommand() string { return "/compact" } // ── MemoryFileProvider ─────────────────────────────────────── func (a *Agent) ProjectMemoryFile() string { workDir := a.GetWorkDir() absDir, err := filepath.Abs(workDir) if err != nil { absDir = workDir } return filepath.Join(absDir, "AGENTS.md") } func (a *Agent) GlobalMemoryFile() string { homeDir, err := os.UserHomeDir() if err != nil { return "" } return filepath.Join(homeDir, ".qoder", "AGENTS.md") }