From 5d544872376468ce07e9dd22ef3dc164fa7fab39 Mon Sep 17 00:00:00 2001 From: shenghui kevin Date: Tue, 3 Mar 2026 06:27:01 -0800 Subject: [PATCH 1/2] feat(agent): add OpenCode support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode (`opencode run --format json`) streams NDJSON events, similar to the Gemini agent. This wires it up so users can drive OpenCode from Telegram / Discord / Slack / etc. - agent/opencode/opencode.go — agent struct, factory, model/mode/provider switching - agent/opencode/session.go — subprocess lifecycle, NDJSON event → core.Event mapping - cmd/cc-connect/main.go — register the "opencode" agent --- agent/opencode/opencode.go | 257 +++++++++++++++++++++++++++++++ agent/opencode/session.go | 301 +++++++++++++++++++++++++++++++++++++ cmd/cc-connect/main.go | 1 + 3 files changed, 559 insertions(+) create mode 100644 agent/opencode/opencode.go create mode 100644 agent/opencode/session.go diff --git a/agent/opencode/opencode.go b/agent/opencode/opencode.go new file mode 100644 index 0000000..156f972 --- /dev/null +++ b/agent/opencode/opencode.go @@ -0,0 +1,257 @@ +package opencode + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + + "github.com/chenhg5/cc-connect/core" +) + +func init() { + core.RegisterAgent("opencode", New) +} + +// Agent drives the OpenCode CLI in headless mode using `opencode run --format json`. +// +// Modes: +// - "default": standard mode +// - "yolo": auto mode (opencode run is auto by default in non-interactive mode) +type Agent struct { + workDir string + model string + mode string + cmd string // CLI binary name, default "opencode" + providers []core.ProviderConfig + activeIdx int + 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) + cmd, _ := opts["cmd"].(string) + if cmd == "" { + cmd = "opencode" + } + + if _, err := exec.LookPath(cmd); err != nil { + return nil, fmt.Errorf("opencode: %q CLI not found in PATH, install from: https://github.com/opencode-ai/opencode", cmd) + } + + return &Agent{ + workDir: workDir, + model: model, + mode: mode, + cmd: cmd, + activeIdx: -1, + }, nil +} + +func normalizeMode(raw string) string { + switch strings.ToLower(strings.TrimSpace(raw)) { + case "yolo", "auto", "force", "bypasspermissions": + return "yolo" + default: + return "default" + } +} + +func (a *Agent) Name() string { return "opencode" } + +func (a *Agent) SetModel(model string) { + a.mu.Lock() + defer a.mu.Unlock() + a.model = model + slog.Info("opencode: 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: "anthropic/claude-sonnet-4-20250514", Desc: "Claude Sonnet 4 (default)"}, + {Name: "anthropic/claude-opus-4-20250514", Desc: "Claude Opus 4"}, + {Name: "openai/gpt-4o", Desc: "GPT-4o"}, + {Name: "openai/o3", Desc: "OpenAI o3"}, + } +} + +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() + model := a.model + mode := a.mode + cmd := a.cmd + workDir := a.workDir + extraEnv := a.providerEnvLocked() + extraEnv = append(extraEnv, a.sessionEnv...) + if a.activeIdx >= 0 && a.activeIdx < len(a.providers) { + if m := a.providers[a.activeIdx].Model; m != "" { + model = m + } + } + a.mu.Unlock() + + return newOpencodeSession(ctx, cmd, workDir, model, mode, sessionID, extraEnv) +} + +// ListSessions runs `opencode session list` and parses the JSON output. +func (a *Agent) ListSessions(_ context.Context) ([]core.AgentSessionInfo, error) { + return listOpencodeSessions(a.cmd, a.workDir) +} + +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("opencode: 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 mode", DescZh: "标准模式"}, + {Key: "yolo", Name: "YOLO", NameZh: "全自动", Desc: "Auto-approve all tool calls", DescZh: "自动批准所有工具调用"}, + } +} + +// -- ContextCompressor -- + +func (a *Agent) CompressCommand() string { return "/compact" } + +// -- MemoryFileProvider -- + +func (a *Agent) ProjectMemoryFile() string { + absDir, err := filepath.Abs(a.workDir) + if err != nil { + absDir = a.workDir + } + return filepath.Join(absDir, "OPENCODE.md") +} + +func (a *Agent) GlobalMemoryFile() string { + homeDir, err := os.UserHomeDir() + if err != nil { + return "" + } + return filepath.Join(homeDir, ".opencode", "OPENCODE.md") +} + +// -- ProviderSwitcher -- + +func (a *Agent) SetProviders(providers []core.ProviderConfig) { + a.mu.Lock() + defer a.mu.Unlock() + a.providers = providers +} + +func (a *Agent) SetActiveProvider(name string) bool { + a.mu.Lock() + defer a.mu.Unlock() + for i, p := range a.providers { + if p.Name == name { + a.activeIdx = i + slog.Info("opencode: provider switched", "provider", name) + return true + } + } + return false +} + +func (a *Agent) GetActiveProvider() *core.ProviderConfig { + a.mu.Lock() + defer a.mu.Unlock() + if a.activeIdx < 0 || a.activeIdx >= len(a.providers) { + return nil + } + p := a.providers[a.activeIdx] + return &p +} + +func (a *Agent) ListProviders() []core.ProviderConfig { + a.mu.Lock() + defer a.mu.Unlock() + result := make([]core.ProviderConfig, len(a.providers)) + copy(result, a.providers) + return result +} + +func (a *Agent) providerEnvLocked() []string { + if a.activeIdx < 0 || a.activeIdx >= len(a.providers) { + return nil + } + p := a.providers[a.activeIdx] + var env []string + if p.APIKey != "" { + env = append(env, "ANTHROPIC_API_KEY="+p.APIKey) + } + for k, v := range p.Env { + env = append(env, k+"="+v) + } + return env +} + +// -- Session listing -- + +// opencodeSessionEntry represents a session from `opencode session list` output. +type opencodeSessionEntry struct { + ID string `json:"id"` + Title string `json:"title"` + Updated string `json:"updated"` +} + +func listOpencodeSessions(cmd, workDir string) ([]core.AgentSessionInfo, error) { + c := exec.Command(cmd, "session", "list", "--format", "json") + c.Dir = workDir + + out, err := c.Output() + if err != nil { + return nil, fmt.Errorf("opencode: session list: %w", err) + } + + var entries []opencodeSessionEntry + if err := json.Unmarshal(out, &entries); err != nil { + return nil, fmt.Errorf("opencode: parse session list: %w", err) + } + + var sessions []core.AgentSessionInfo + for _, e := range entries { + sessions = append(sessions, core.AgentSessionInfo{ + ID: e.ID, + Summary: e.Title, + }) + } + + return sessions, nil +} diff --git a/agent/opencode/session.go b/agent/opencode/session.go new file mode 100644 index 0000000..6171218 --- /dev/null +++ b/agent/opencode/session.go @@ -0,0 +1,301 @@ +package opencode + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "os/exec" + "sync" + "sync/atomic" + "unicode/utf8" + + "github.com/chenhg5/cc-connect/core" +) + +// opencodeSession manages multi-turn conversations with the OpenCode CLI. +// Each Send() launches a new `opencode run --format json` process +// with --session for conversation continuity. +type opencodeSession struct { + cmd string + workDir string + model string + mode string + extraEnv []string + events chan core.Event + chatID atomic.Value // stores string — OpenCode session ID + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + alive atomic.Bool +} + +func newOpencodeSession(ctx context.Context, cmd, workDir, model, mode, resumeID string, extraEnv []string) (*opencodeSession, error) { + sessionCtx, cancel := context.WithCancel(ctx) + + s := &opencodeSession{ + cmd: cmd, + workDir: workDir, + model: model, + mode: mode, + extraEnv: extraEnv, + events: make(chan core.Event, 64), + ctx: sessionCtx, + cancel: cancel, + } + s.alive.Store(true) + + if resumeID != "" { + s.chatID.Store(resumeID) + } + + return s, nil +} + +func (s *opencodeSession) Send(prompt string, images []core.ImageAttachment) error { + if !s.alive.Load() { + return fmt.Errorf("session is closed") + } + + chatID := s.CurrentSessionID() + isResume := chatID != "" + + args := []string{"run", "--format", "json"} + + if isResume { + args = append(args, "--session", chatID) + } + if s.model != "" { + args = append(args, "--model", s.model) + } + if s.workDir != "" { + args = append(args, "--dir", s.workDir) + } + + // Enable thinking blocks + args = append(args, "--thinking") + + // Append prompt as positional arg + args = append(args, prompt) + + slog.Debug("opencodeSession: launching", "resume", isResume, "args", args) + + cmd := exec.CommandContext(s.ctx, s.cmd, args...) + cmd.Dir = s.workDir + env := os.Environ() + if len(s.extraEnv) > 0 { + env = core.MergeEnv(env, s.extraEnv) + } + cmd.Env = env + + stdout, err := cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("opencodeSession: stdout pipe: %w", err) + } + + var stderrBuf bytes.Buffer + cmd.Stderr = &stderrBuf + + if err := cmd.Start(); err != nil { + return fmt.Errorf("opencodeSession: start: %w", err) + } + + s.wg.Add(1) + go s.readLoop(cmd, stdout, &stderrBuf) + + return nil +} + +func (s *opencodeSession) readLoop(cmd *exec.Cmd, stdout io.ReadCloser, stderrBuf *bytes.Buffer) { + defer s.wg.Done() + defer func() { + if err := cmd.Wait(); err != nil { + stderrMsg := stderrBuf.String() + if stderrMsg != "" { + slog.Error("opencodeSession: process failed", "error", err, "stderr", stderrMsg) + s.events <- core.Event{Type: core.EventError, Error: fmt.Errorf("%s", stderrMsg)} + } + } + }() + + scanner := bufio.NewScanner(stdout) + scanner.Buffer(make([]byte, 1024*1024), 1024*1024) + + for scanner.Scan() { + line := scanner.Text() + if line == "" { + continue + } + + var raw map[string]any + if err := json.Unmarshal([]byte(line), &raw); err != nil { + slog.Debug("opencodeSession: non-JSON line", "line", line) + continue + } + + s.handleEvent(raw) + } + + if err := scanner.Err(); err != nil { + slog.Error("opencodeSession: scanner error", "error", err) + s.events <- core.Event{Type: core.EventError, Error: fmt.Errorf("read stdout: %w", err)} + } +} + +// OpenCode NDJSON event structure: +// +// { "type": "text|tool_use|reasoning|step_start|step_finish", +// "part": { "type": "text|tool|reasoning|step-start|step-finish", ... } } +func (s *opencodeSession) handleEvent(raw map[string]any) { + eventType, _ := raw["type"].(string) + + switch eventType { + case "text": + s.handleText(raw) + case "tool_use": + s.handleToolUse(raw) + case "reasoning": + s.handleReasoning(raw) + case "step_start": + s.handleStepStart(raw) + case "step_finish": + s.handleStepFinish(raw) + default: + slog.Debug("opencodeSession: unhandled event", "type", eventType) + } +} + +func (s *opencodeSession) handleText(raw map[string]any) { + part, _ := raw["part"].(map[string]any) + if part == nil { + return + } + text, _ := part["text"].(string) + if text != "" { + s.events <- core.Event{ + Type: core.EventText, + Content: text, + } + } +} + +func (s *opencodeSession) handleToolUse(raw map[string]any) { + part, _ := raw["part"].(map[string]any) + if part == nil { + return + } + + toolName, _ := part["tool"].(string) + + // Check state for status + state, _ := part["state"].(map[string]any) + status := "" + if state != nil { + status, _ = state["status"].(string) + } + + if status == "completed" { + // Tool result + output := "" + if state != nil { + output, _ = state["output"].(string) + } + s.events <- core.Event{ + Type: core.EventToolResult, + ToolName: toolName, + Content: truncate(output, 500), + } + } else { + // Tool use (running or starting) + input := "" + if state != nil { + inputVal, _ := state["input"].(string) + if inputVal != "" { + input = inputVal + } else { + // Try to marshal input as JSON summary + if inputMap, ok := state["input"].(map[string]any); ok { + b, _ := json.Marshal(inputMap) + input = truncate(string(b), 200) + } + } + } + s.events <- core.Event{ + Type: core.EventToolUse, + ToolName: toolName, + ToolInput: input, + } + } +} + +func (s *opencodeSession) handleReasoning(raw map[string]any) { + part, _ := raw["part"].(map[string]any) + if part == nil { + return + } + text, _ := part["text"].(string) + if text != "" { + s.events <- core.Event{ + Type: core.EventThinking, + Content: text, + } + } +} + +func (s *opencodeSession) handleStepStart(raw map[string]any) { + part, _ := raw["part"].(map[string]any) + if part == nil { + return + } + sessionID, _ := part["sessionID"].(string) + if sessionID != "" { + s.chatID.Store(sessionID) + slog.Debug("opencodeSession: session started", "session_id", sessionID) + } +} + +func (s *opencodeSession) handleStepFinish(_ map[string]any) { + sid := s.CurrentSessionID() + s.events <- core.Event{ + Type: core.EventResult, + SessionID: sid, + Done: true, + } +} + +// RespondPermission is a no-op — OpenCode handles permissions internally. +func (s *opencodeSession) RespondPermission(_ string, _ core.PermissionResult) error { + return nil +} + +func (s *opencodeSession) Events() <-chan core.Event { + return s.events +} + +func (s *opencodeSession) CurrentSessionID() string { + v, _ := s.chatID.Load().(string) + return v +} + +func (s *opencodeSession) Alive() bool { + return s.alive.Load() +} + +func (s *opencodeSession) Close() error { + s.alive.Store(false) + s.cancel() + s.wg.Wait() + close(s.events) + return nil +} + +func truncate(s string, maxRunes int) string { + if utf8.RuneCountInString(s) <= maxRunes { + return s + } + return string([]rune(s)[:maxRunes]) + "..." +} diff --git a/cmd/cc-connect/main.go b/cmd/cc-connect/main.go index 9969fe9..887aeb1 100644 --- a/cmd/cc-connect/main.go +++ b/cmd/cc-connect/main.go @@ -22,6 +22,7 @@ import ( _ "github.com/chenhg5/cc-connect/agent/codex" _ "github.com/chenhg5/cc-connect/agent/cursor" _ "github.com/chenhg5/cc-connect/agent/gemini" + _ "github.com/chenhg5/cc-connect/agent/opencode" _ "github.com/chenhg5/cc-connect/agent/qoder" _ "github.com/chenhg5/cc-connect/platform/dingtalk" From f2d0d6c46c8c9db063105a4e23746864cc467b6e Mon Sep 17 00:00:00 2001 From: shenghui kevin Date: Tue, 3 Mar 2026 06:33:07 -0800 Subject: [PATCH 2/2] docs: update README and config for OpenCode agent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add OpenCode to support matrix (4 agents → 5 agents) - Add OpenCode permission modes section - Add OpenCode to prerequisites, provider env mapping, project structure - Add OpenCode config example in config.example.toml - Update both English and Chinese README --- README.md | 28 ++++++++++++++++++++++------ README.zh-CN.md | 28 ++++++++++++++++++++++------ config.example.toml | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f3e9973..b8344d0 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,15 @@ cc-connect bridges AI coding assistants running on your dev machine to the messa └────────────┘ ┌─────┼─────┐ ▼ ▼ ▼ - Claude Gemini Codex ...5 agents - Code CLI + Claude Gemini Codex ...6 agents + Code CLI OpenCode ``` ### Why cc-connect? > Time to uninstall OpenClaw — cc-connect gives you access to the most powerful coding agents available, not just one. -- **5 AI Agents** — Claude Code, Codex, Cursor Agent, Qoder CLI, Gemini CLI. Use whichever fits your workflow, or all of them at once. +- **6 AI Agents** — Claude Code, Codex, Cursor Agent, Qoder CLI, Gemini CLI, OpenCode. Use whichever fits your workflow, or all of them at once. - **8 Chat Platforms** — Feishu, DingTalk, Slack, Telegram, Discord, WeChat Work, LINE, QQ. Most need zero public IP. - **Full Control from Chat** — Switch models (`/model`), change permission modes (`/mode`), manage sessions, all via slash commands. - **Agent Memory** — Read and write agent instruction files (`/memory`) without touching the terminal. @@ -49,7 +49,8 @@ cc-connect bridges AI coding assistants running on your dev machine to the messa | Agent | Cursor Agent | ✅ Supported | | Agent | Gemini CLI (Google) | ✅ Supported | | Agent | Qoder CLI | ✅ Supported | -| Agent | Crush / OpenCode | 🔜 Planned | +| Agent | OpenCode | ✅ Supported | +| Agent | Crush | 🔜 Planned | | Agent | Goose (Block) | 🔜 Planned | | Agent | Aider | 🔜 Planned | | Agent | Kimi Code (Moonshot) | 🔭 Exploring | @@ -81,7 +82,8 @@ cc-connect bridges AI coding assistants running on your dev machine to the messa - **Codex**: [Codex CLI](https://github.com/openai/codex) installed (`npm install -g @openai/codex`), OR - **Cursor Agent**: [Cursor Agent CLI](https://docs.cursor.com/agent) installed (`agent --version` to verify), OR - **Gemini CLI**: [Gemini CLI](https://github.com/google-gemini/gemini-cli) installed (`npm install -g @google/gemini-cli`), OR -- **Qoder CLI**: [Qoder CLI](https://qoder.com) installed (`curl -fsSL https://qoder.com/install | bash`) +- **Qoder CLI**: [Qoder CLI](https://qoder.com) installed (`curl -fsSL https://qoder.com/install | bash`), OR +- **OpenCode**: [OpenCode](https://github.com/opencode-ai/opencode) installed (`opencode --version` to verify) ### Install & Configure via AI Agent (Recommended) @@ -274,6 +276,13 @@ All agents support permission modes switchable at runtime via `/mode`. | **Default** | `default` | Standard permissions; prompt for approval. | | **YOLO** | `yolo` | Skip all permission checks, auto-approve. | +**OpenCode** modes: + +| Mode | Config Value | Behavior | +|------|-------------|----------| +| **Default** | `default` | Standard mode. | +| **YOLO** | `yolo` | Auto-approve all tool calls. | + ```toml # Claude Code [projects.agent.options] @@ -296,6 +305,10 @@ mode = "default" # Qoder CLI [projects.agent.options] mode = "default" + +# OpenCode +[projects.agent.options] +mode = "default" ``` Switch mode at runtime from the chat: @@ -375,6 +388,7 @@ Adding, removing, and switching providers all persist to `config.toml` automatic | Claude Code | `ANTHROPIC_API_KEY` | `ANTHROPIC_BASE_URL` | | Codex | `OPENAI_API_KEY` | `OPENAI_BASE_URL` | | Gemini CLI | `GEMINI_API_KEY` | — (use `env` map) | +| OpenCode | `ANTHROPIC_API_KEY` | — (use `env` map) | The `env` map in provider config lets you set arbitrary environment variables for any setup (Bedrock, Vertex, Azure, custom proxies, etc.). @@ -473,6 +487,7 @@ Claude Code will automatically translate your request into a `cc-connect cron ad | Cursor | `.cursorrules` | | Qoder CLI | `AGENTS.md` (project), `~/.qoder/AGENTS.md` (global) | | Gemini CLI | `GEMINI.md` | +| OpenCode | `OPENCODE.md` | **Content to add:** @@ -649,7 +664,8 @@ cc-connect/ │ ├── codex/ # OpenAI Codex CLI (exec --json) │ ├── cursor/ # Cursor Agent CLI (--print stream-json) │ ├── qoder/ # Qoder CLI (-p -f stream-json) -│ └── gemini/ # Gemini CLI (-p --output-format stream-json) +│ ├── gemini/ # Gemini CLI (-p --output-format stream-json) +│ └── opencode/ # OpenCode (run --format json) ├── docs/ # Platform setup guides ├── config.example.toml # Config template ├── INSTALL.md # AI-agent-friendly install guide diff --git a/README.zh-CN.md b/README.zh-CN.md index 6497217..3ba27bc 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -20,15 +20,15 @@ cc-connect 把运行在你开发机上的 AI 编程助手桥接到你日常使 └────────────┘ ┌─────┼─────┐ ▼ ▼ ▼ - Claude Gemini Codex ...5 个 Agent - Code CLI + Claude Gemini Codex ...6 个 Agent + Code CLI OpenCode ``` ### 核心亮点 > 是时候卸载 OpenClaw 了 — cc-connect 让你同时拥有最强的那几个 AI 编程助手,而不只是一个。 -- **5 大 AI Agent** — Claude Code、Codex、Cursor Agent、Qoder CLI、Gemini CLI,按需选用,也可以同时使用 +- **6 大 AI Agent** — Claude Code、Codex、Cursor Agent、Qoder CLI、Gemini CLI、OpenCode,按需选用,也可以同时使用 - **8 大聊天平台** — 飞书、钉钉、Slack、Telegram、Discord、企业微信、LINE、QQ,大部分无需公网 IP - **聊天即控制** — 切换模型 `/model`、切换权限 `/mode`、管理会话,全部通过斜杠命令完成 - **Agent 记忆** — 在聊天中直接读写 Agent 指令文件 `/memory`,无需回到终端 @@ -54,7 +54,8 @@ cc-connect 把运行在你开发机上的 AI 编程助手桥接到你日常使 | Agent | Cursor Agent | ✅ 已支持 | | Agent | Gemini CLI (Google) | ✅ 已支持 | | Agent | Qoder CLI | ✅ 已支持 | -| Agent | Crush / OpenCode | 🔜 计划中 | +| Agent | OpenCode | ✅ 已支持 | +| Agent | Crush | 🔜 计划中 | | Agent | Goose (Block) | 🔜 计划中 | | Agent | Aider | 🔜 计划中 | | Agent | Kimi Code (月之暗面) | 🔭 探索中 | @@ -86,7 +87,8 @@ cc-connect 把运行在你开发机上的 AI 编程助手桥接到你日常使 - **Codex**: [Codex CLI](https://github.com/openai/codex) 已安装(`npm install -g @openai/codex`),或 - **Cursor Agent**: [Cursor Agent CLI](https://docs.cursor.com/agent) 已安装(`agent --version` 验证),或 - **Gemini CLI**: [Gemini CLI](https://github.com/google-gemini/gemini-cli) 已安装(`npm install -g @google/gemini-cli`),或 -- **Qoder CLI**: [Qoder CLI](https://qoder.com) 已安装(`curl -fsSL https://qoder.com/install | bash`) +- **Qoder CLI**: [Qoder CLI](https://qoder.com) 已安装(`curl -fsSL https://qoder.com/install | bash`),或 +- **OpenCode**: [OpenCode](https://github.com/opencode-ai/opencode) 已安装(`opencode --version` 验证) ### 通过 AI Agent 安装配置(推荐) @@ -278,6 +280,13 @@ allow_from = "*" # 允许的 QQ 号,如 "12345,67890","*" 表示所有 | **标准权限** | `default` | 标准权限,每次工具调用需确认。 | | **YOLO 模式** | `yolo` | 跳过所有权限检查,自动批准。 | +**OpenCode** 模式: + +| 模式 | 配置值 | 行为 | +|------|--------|------| +| **默认** | `default` | 标准模式。 | +| **全自动** | `yolo` | 自动批准所有工具调用。 | + ```toml # Claude Code [projects.agent.options] @@ -300,6 +309,10 @@ mode = "default" # Qoder CLI [projects.agent.options] mode = "default" + +# OpenCode +[projects.agent.options] +mode = "default" ``` 在聊天中切换模式: @@ -379,6 +392,7 @@ cc-connect provider import --db-path ~/.cc-switch/cc-switch.db # 指定数据 | Claude Code | `ANTHROPIC_API_KEY` | `ANTHROPIC_BASE_URL` | | Codex | `OPENAI_API_KEY` | `OPENAI_BASE_URL` | | Gemini CLI | `GEMINI_API_KEY` | —(使用 `env` 字段)| +| OpenCode | `ANTHROPIC_API_KEY` | —(使用 `env` 字段)| Provider 配置中的 `env` 字段支持设置任意环境变量,可用于 Bedrock、Vertex、Azure、自定义代理等各种场景。 @@ -477,6 +491,7 @@ Claude Code 会通过 `--append-system-prompt` 自动将你的请求转为 `cc-c | Cursor | `.cursorrules` | | Qoder CLI | `AGENTS.md`(项目级)、`~/.qoder/AGENTS.md`(全局) | | Gemini CLI | `GEMINI.md` | +| OpenCode | `OPENCODE.md` | **需要添加的内容:** @@ -653,7 +668,8 @@ cc-connect/ │ ├── codex/ # OpenAI Codex CLI(exec --json) │ ├── cursor/ # Cursor Agent CLI(--print stream-json) │ ├── qoder/ # Qoder CLI(-p -f stream-json) -│ └── gemini/ # Gemini CLI(-p --output-format stream-json) +│ ├── gemini/ # Gemini CLI(-p --output-format stream-json) +│ └── opencode/ # OpenCode(run --format json) ├── docs/ # 平台接入指南 ├── config.example.toml # 配置模板 ├── INSTALL.md # AI agent 友好的安装配置指南 diff --git a/config.example.toml b/config.example.toml index 79a4e4c..6860326 100644 --- a/config.example.toml +++ b/config.example.toml @@ -458,7 +458,38 @@ app_secret = "your-feishu-app-secret" # token = "your-telegram-bot-token" # ============================================================================= -# Project 6: Another example / 项目 6:更多示例 (uncomment to enable / 取消注释以启用) +# Project 6: Using OpenCode / 项目 6:使用 OpenCode (uncomment to enable / 取消注释以启用) +# ============================================================================= +# Requires: OpenCode CLI — https://github.com/opencode-ai/opencode +# 需要安装:OpenCode CLI +# Uses `opencode run --format json` under the hood. +# 底层使用 `opencode run --format json` 命令。 + +# [[projects]] +# name = "my-opencode-project" +# +# [projects.agent] +# type = "opencode" +# +# [projects.agent.options] +# work_dir = "/path/to/project" +# mode = "default" # "default" | "yolo" +# +# Mode options / 模式说明: +# - "default": Standard mode / 标准模式 +# - "yolo": Auto-approve all tool calls / 自动批准所有工具调用 +# +# Optional: specify a model (provider/model format) / 可选:指定模型 +# model = "anthropic/claude-sonnet-4-20250514" +# +# [[projects.platforms]] +# type = "telegram" +# +# [projects.platforms.options] +# token = "your-telegram-bot-token" + +# ============================================================================= +# Project 7: Another example / 项目 7:更多示例 (uncomment to enable / 取消注释以启用) # ============================================================================= # [[projects]]