From dbc95abb77e635a7c2fb388001e727f7158f88df Mon Sep 17 00:00:00 2001 From: Joseph19820124 <164839249+Joseph19820124@users.noreply.github.com> Date: Fri, 8 May 2026 02:08:33 +0800 Subject: [PATCH 1/2] feat: add GG Coder CLI support (ACP via --rpc) - Add Dockerfile.ggcoder (node:22 + @kenkaiiii/ggcoder) - Add ggcoder example to values.yaml and NOTES.txt auth hint - Add ggcoder to CI build/smoke-test/pr-preview matrices - Add docs/ggcoder.md with install and auth instructions - Add ggcoder row to README agents table --- .github/workflows/build.yml | 3 ++ .github/workflows/docker-smoke-test.yml | 1 + .github/workflows/pr-preview.yml | 1 + Dockerfile.ggcoder | 36 +++++++++++++++ README.md | 1 + charts/openab/templates/NOTES.txt | 3 ++ charts/openab/values.yaml | 28 ++++++++++++ docs/ggcoder.md | 61 +++++++++++++++++++++++++ 8 files changed, 134 insertions(+) create mode 100644 Dockerfile.ggcoder create mode 100644 docs/ggcoder.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b360417..26291da6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,7 @@ jobs: - { suffix: "-codex", dockerfile: "Dockerfile.codex", artifact: "codex" } - { suffix: "-claude", dockerfile: "Dockerfile.claude", artifact: "claude" } - { suffix: "-gemini", dockerfile: "Dockerfile.gemini", artifact: "gemini" } + - { suffix: "-ggcoder", dockerfile: "Dockerfile.ggcoder", artifact: "ggcoder" } - { suffix: "-copilot", dockerfile: "Dockerfile.copilot", artifact: "copilot" } - { suffix: "-opencode", dockerfile: "Dockerfile.opencode", artifact: "opencode" } - { suffix: "-cursor", dockerfile: "Dockerfile.cursor", artifact: "cursor" } @@ -132,6 +133,7 @@ jobs: - { suffix: "-codex", artifact: "codex" } - { suffix: "-claude", artifact: "claude" } - { suffix: "-gemini", artifact: "gemini" } + - { suffix: "-ggcoder", artifact: "ggcoder" } - { suffix: "-copilot", artifact: "copilot" } - { suffix: "-opencode", artifact: "opencode" } - { suffix: "-cursor", artifact: "cursor" } @@ -182,6 +184,7 @@ jobs: - { suffix: "-codex" } - { suffix: "-claude" } - { suffix: "-gemini" } + - { suffix: "-ggcoder" } - { suffix: "-copilot" } - { suffix: "-opencode" } - { suffix: "-cursor" } diff --git a/.github/workflows/docker-smoke-test.yml b/.github/workflows/docker-smoke-test.yml index 64b4653a..a7340e62 100644 --- a/.github/workflows/docker-smoke-test.yml +++ b/.github/workflows/docker-smoke-test.yml @@ -17,6 +17,7 @@ jobs: - { dockerfile: Dockerfile.claude, suffix: "-claude", agent: "claude-agent-acp", agent_args: "" } - { dockerfile: Dockerfile.codex, suffix: "-codex", agent: "codex-acp", agent_args: "" } - { dockerfile: Dockerfile.gemini, suffix: "-gemini", agent: "gemini", agent_args: "--acp" } + - { dockerfile: Dockerfile.ggcoder, suffix: "-ggcoder", agent: "ggcoder", agent_args: "--rpc" } - { dockerfile: Dockerfile.copilot, suffix: "-copilot", agent: "copilot", agent_args: "--acp" } - { dockerfile: Dockerfile.opencode, suffix: "-opencode", agent: "opencode", agent_args: "acp" } - { dockerfile: Dockerfile.cursor, suffix: "-cursor", agent: "cursor-agent", agent_args: "acp" } diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 69ba8907..3634275d 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -18,6 +18,7 @@ on: - copilot - cursor - gemini + - ggcoder - opencode default: 'default' diff --git a/Dockerfile.ggcoder b/Dockerfile.ggcoder new file mode 100644 index 00000000..71d2e53f --- /dev/null +++ b/Dockerfile.ggcoder @@ -0,0 +1,36 @@ +# --- Build stage --- +FROM rust:1-bookworm AS builder +WORKDIR /build +COPY Cargo.toml Cargo.lock ./ +RUN mkdir src && echo 'fn main() {}' > src/main.rs && cargo build --release && rm -rf src +COPY src/ src/ +RUN touch src/main.rs && cargo build --release + +# --- Runtime stage --- +FROM node:22-bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps ripgrep tini && rm -rf /var/lib/apt/lists/* + +# Install GG Coder CLI (ACP via --rpc) +ARG GGCODER_VERSION=4.3.155 +RUN npm install -g @kenkaiiii/ggcoder@${GGCODER_VERSION} --retry 3 + +# Install gh CLI +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ + -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list && \ + apt-get update && apt-get install -y --no-install-recommends gh && \ + rm -rf /var/lib/apt/lists/* + +ENV HOME=/home/node +WORKDIR /home/node + +COPY --from=builder --chown=node:node /build/target/release/openab /usr/local/bin/openab + +RUN mkdir -p /home/node/.config/ggcoder && chown -R node:node /home/node/.config + +USER node +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD pgrep -x openab || exit 1 +ENTRYPOINT ["tini", "--"] +CMD ["openab", "run", "-c", "/etc/openab/config.toml"] diff --git a/README.md b/README.md index 4dd3d4f2..011c0a75 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ The bot creates a thread. After that, just type in the thread — no @mention ne | Claude Code | `claude-agent-acp` | [@agentclientprotocol/claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp) | [docs/claude-code.md](docs/claude-code.md) | | Codex | `codex-acp` | [@zed-industries/codex-acp](https://github.com/zed-industries/codex-acp) | [docs/codex.md](docs/codex.md) | | Gemini | `gemini --acp` | Native | [docs/gemini.md](docs/gemini.md) | +| GG Coder | `ggcoder --rpc` | Native | [docs/ggcoder.md](docs/ggcoder.md) | | OpenCode | `opencode acp` | Native | [docs/opencode.md](docs/opencode.md) | | Copilot CLI ⚠️ | `copilot --acp --stdio` | Native | [docs/copilot.md](docs/copilot.md) | | Cursor | `cursor-agent acp` | Native | [docs/cursor.md](docs/cursor.md) | diff --git a/charts/openab/templates/NOTES.txt b/charts/openab/templates/NOTES.txt index 2030ed6f..a66175f3 100644 --- a/charts/openab/templates/NOTES.txt +++ b/charts/openab/templates/NOTES.txt @@ -36,6 +36,9 @@ Agents deployed: {{- else if eq (toString $cfg.command) "gemini" }} Authenticate: kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- gemini +{{- else if eq (toString $cfg.command) "ggcoder" }} + Authenticate: + kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- ggcoder login {{- else if eq (toString $cfg.command) "opencode" }} Authenticate: kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- opencode auth login diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 8a83e963..63d1c5c0 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -109,6 +109,34 @@ agents: # agentsMd: "" # resources: {} # image: "ghcr.io/openabdev/openab-opencode:latest" + # ggcoder: + # command: ggcoder + # args: + # - --rpc + # discord: + # enabled: true + # allowedChannels: + # - "YOUR_CHANNEL_ID" + # allowedUsers: [] + # allowBotMessages: "off" + # trustedBotIds: [] + # workingDir: /home/node + # env: {} + # envFrom: [] + # secretEnv: [] + # pool: + # maxSessions: 10 + # sessionTtlHours: 24 + # reactions: + # enabled: true + # removeAfterReply: false + # persistence: + # enabled: true + # storageClass: "" + # size: 1Gi + # agentsMd: "" + # resources: {} + # image: "ghcr.io/joseph19820124/openab-ggcoder:latest" # cursor: # command: cursor-agent # args: diff --git a/docs/ggcoder.md b/docs/ggcoder.md new file mode 100644 index 00000000..f03de6ce --- /dev/null +++ b/docs/ggcoder.md @@ -0,0 +1,61 @@ +# GG Coder CLI + +GG Coder supports ACP via the `--rpc` flag (JSON-RPC mode for IDE integrations). + +## Docker Image + +```bash +docker build -f Dockerfile.ggcoder -t openab-ggcoder:latest . +``` + +The image installs `@kenkaiiii/ggcoder` globally via npm. + +## Helm Install + +```bash +helm install openab openab/openab \ + --set agents.kiro.enabled=false \ + --set agents.ggcoder.discord.enabled=true \ + --set agents.ggcoder.discord.botToken="$DISCORD_BOT_TOKEN" \ + --set-string 'agents.ggcoder.discord.allowedChannels[0]=YOUR_CHANNEL_ID' \ + --set agents.ggcoder.image=ghcr.io/joseph19820124/openab-ggcoder:latest \ + --set agents.ggcoder.command=ggcoder \ + --set agents.ggcoder.args='{--rpc}' \ + --set agents.ggcoder.workingDir=/home/node +``` + +> Set `agents.kiro.enabled=false` to disable the default Kiro agent. +> +> (Optional) Use `--set agents.ggcoder.args='{--provider,anthropic,--rpc}'` to specify a provider explicitly. + +## Manual config.toml + +```toml +[agent] +command = "ggcoder" +args = ["--rpc"] +working_dir = "/home/node" +``` + +## Authentication + +GG Coder supports Anthropic and OpenAI OAuth: + +- **Anthropic**: Run `ggcoder login` inside the pod and follow the OAuth flow +- **OpenAI**: Run `ggcoder login --provider openai` inside the pod +- **API key via env**: Set `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` + +```bash +# Interactive login (OAuth) +kubectl exec -it deployment/-ggcoder -- ggcoder login + +# Or set API key via Helm +helm upgrade openab/openab \ + --set agents.ggcoder.env.ANTHROPIC_API_KEY="" +``` + +After authenticating, restart the deployment: + +```bash +kubectl rollout restart deployment/-ggcoder +``` From abe616dc5d954528c0450386a36c792d6c6bc4e6 Mon Sep 17 00:00:00 2001 From: Joseph19820124 <164839249+Joseph19820124@users.noreply.github.com> Date: Fri, 8 May 2026 02:32:54 +0800 Subject: [PATCH 2/2] feat: add GG Coder ACP wrapper --- .github/workflows/docker-smoke-test.yml | 3 +- Dockerfile.ggcoder | 4 +- README.md | 2 +- bin/ggcoder-acp.mjs | 281 ++++++++++++++++++++++++ charts/openab/templates/NOTES.txt | 2 +- charts/openab/values.yaml | 5 +- docs/ggcoder.md | 17 +- src/setup/config.rs | 1 + src/setup/validate.rs | 4 +- src/setup/wizard.rs | 13 +- 10 files changed, 315 insertions(+), 17 deletions(-) create mode 100755 bin/ggcoder-acp.mjs diff --git a/.github/workflows/docker-smoke-test.yml b/.github/workflows/docker-smoke-test.yml index a7340e62..5de374cb 100644 --- a/.github/workflows/docker-smoke-test.yml +++ b/.github/workflows/docker-smoke-test.yml @@ -4,6 +4,7 @@ on: pull_request: paths: - 'Dockerfile*' + - 'bin/**' - 'src/**' - 'Cargo.*' @@ -17,7 +18,7 @@ jobs: - { dockerfile: Dockerfile.claude, suffix: "-claude", agent: "claude-agent-acp", agent_args: "" } - { dockerfile: Dockerfile.codex, suffix: "-codex", agent: "codex-acp", agent_args: "" } - { dockerfile: Dockerfile.gemini, suffix: "-gemini", agent: "gemini", agent_args: "--acp" } - - { dockerfile: Dockerfile.ggcoder, suffix: "-ggcoder", agent: "ggcoder", agent_args: "--rpc" } + - { dockerfile: Dockerfile.ggcoder, suffix: "-ggcoder", agent: "ggcoder-acp", agent_args: "" } - { dockerfile: Dockerfile.copilot, suffix: "-copilot", agent: "copilot", agent_args: "--acp" } - { dockerfile: Dockerfile.opencode, suffix: "-opencode", agent: "opencode", agent_args: "acp" } - { dockerfile: Dockerfile.cursor, suffix: "-cursor", agent: "cursor-agent", agent_args: "acp" } diff --git a/Dockerfile.ggcoder b/Dockerfile.ggcoder index 71d2e53f..e95fbbff 100644 --- a/Dockerfile.ggcoder +++ b/Dockerfile.ggcoder @@ -10,7 +10,8 @@ RUN touch src/main.rs && cargo build --release FROM node:22-bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl procps ripgrep tini && rm -rf /var/lib/apt/lists/* -# Install GG Coder CLI (ACP via --rpc) +# Install GG Coder CLI. OpenAB talks ACP to the ggcoder-acp wrapper, which +# translates to GG Coder's JSON-RPC mode. ARG GGCODER_VERSION=4.3.155 RUN npm install -g @kenkaiiii/ggcoder@${GGCODER_VERSION} --retry 3 @@ -26,6 +27,7 @@ ENV HOME=/home/node WORKDIR /home/node COPY --from=builder --chown=node:node /build/target/release/openab /usr/local/bin/openab +COPY --chown=node:node bin/ggcoder-acp.mjs /usr/local/bin/ggcoder-acp RUN mkdir -p /home/node/.config/ggcoder && chown -R node:node /home/node/.config diff --git a/README.md b/README.md index 011c0a75..aa1bc795 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ The bot creates a thread. After that, just type in the thread — no @mention ne | Claude Code | `claude-agent-acp` | [@agentclientprotocol/claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp) | [docs/claude-code.md](docs/claude-code.md) | | Codex | `codex-acp` | [@zed-industries/codex-acp](https://github.com/zed-industries/codex-acp) | [docs/codex.md](docs/codex.md) | | Gemini | `gemini --acp` | Native | [docs/gemini.md](docs/gemini.md) | -| GG Coder | `ggcoder --rpc` | Native | [docs/ggcoder.md](docs/ggcoder.md) | +| GG Coder | `ggcoder-acp` | OpenAB wrapper around `ggcoder --rpc` | [docs/ggcoder.md](docs/ggcoder.md) | | OpenCode | `opencode acp` | Native | [docs/opencode.md](docs/opencode.md) | | Copilot CLI ⚠️ | `copilot --acp --stdio` | Native | [docs/copilot.md](docs/copilot.md) | | Cursor | `cursor-agent acp` | Native | [docs/cursor.md](docs/cursor.md) | diff --git a/bin/ggcoder-acp.mjs b/bin/ggcoder-acp.mjs new file mode 100755 index 00000000..04021d4a --- /dev/null +++ b/bin/ggcoder-acp.mjs @@ -0,0 +1,281 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import { createInterface } from "node:readline"; + +const passthroughArgs = process.argv.slice(2).filter((arg) => arg !== "--rpc"); +let sessionId = null; +let rpc = null; +let nextRpcId = 1; +const pending = new Map(); +const toolNames = new Map(); + +function send(message) { + process.stdout.write(`${JSON.stringify(message)}\n`); +} + +function result(id, value) { + send({ jsonrpc: "2.0", id, result: value }); +} + +function error(id, code, message) { + send({ jsonrpc: "2.0", id, error: { code, message } }); +} + +function notify(update) { + if (!sessionId) return; + send({ + jsonrpc: "2.0", + method: "session/update", + params: { sessionId, update }, + }); +} + +function flattenPrompt(prompt) { + if (!Array.isArray(prompt)) return ""; + return prompt + .map((block) => { + if (block?.type === "text") return block.text ?? ""; + if (block?.type === "image") return "[Image attachment omitted: GG Coder RPC accepts text only.]"; + return `[Unsupported content block: ${JSON.stringify(block)}]`; + }) + .filter(Boolean) + .join("\n\n"); +} + +function toolTitle(name, args) { + if (!args || Object.keys(args).length === 0) return name; + const detail = args.command ?? args.file_path ?? args.path ?? args.pattern ?? ""; + return detail ? `${name}: ${String(detail).slice(0, 120)}` : name; +} + +function handleRpcEvent(event) { + if (event.type === "ready") { + rpc.ready = true; + for (const resolve of rpc.readyWaiters) resolve(); + rpc.readyWaiters = []; + return; + } + + if (event.type === "result" || event.type === "error") { + const acpId = pending.get(event.id); + if (acpId === undefined) return; + pending.delete(event.id); + if (event.type === "error") { + error(acpId, -32000, event.message ?? "GG Coder RPC error"); + } else { + result(acpId, event.data ?? {}); + } + return; + } + + if (event.type === "text_delta" && event.text) { + notify({ + sessionUpdate: "agent_message_chunk", + content: { type: "text", text: event.text }, + }); + return; + } + + if (event.type === "thinking_delta") { + notify({ + sessionUpdate: "agent_thought_chunk", + content: { type: "text", text: event.text ?? "" }, + }); + return; + } + + if (event.type === "tool_call_start") { + toolNames.set(event.toolCallId, event.name); + notify({ + sessionUpdate: "tool_call", + toolCallId: event.toolCallId, + title: toolTitle(event.name, event.args), + }); + return; + } + + if (event.type === "tool_call_update") { + notify({ + sessionUpdate: "tool_call_update", + toolCallId: event.toolCallId, + title: toolNames.get(event.toolCallId) ?? "Tool", + status: "running", + }); + return; + } + + if (event.type === "tool_call_end") { + notify({ + sessionUpdate: "tool_call_update", + toolCallId: event.toolCallId, + title: toolNames.get(event.toolCallId) ?? "Tool", + status: event.isError ? "failed" : "completed", + }); + toolNames.delete(event.toolCallId); + return; + } + + if (event.type === "error" && event.message) { + notify({ + sessionUpdate: "agent_message_chunk", + content: { type: "text", text: `GG Coder error: ${event.message}` }, + }); + } +} + +function startRpc(cwd) { + if (rpc) return rpc; + + const child = spawn("ggcoder", [...passthroughArgs, "--rpc"], { + cwd, + env: process.env, + stdio: ["pipe", "pipe", "inherit"], + }); + + rpc = { + child, + ready: false, + exitError: null, + readyWaiters: [], + }; + + child.on("error", (err) => { + if (rpc) rpc.exitError = err; + const message = `failed to start GG Coder RPC: ${err.message}`; + for (const acpId of pending.values()) error(acpId, -32000, message); + pending.clear(); + for (const resolve of rpc?.readyWaiters ?? []) resolve(); + }); + + child.on("exit", (code, signal) => { + const message = `GG Coder RPC exited${signal ? ` by ${signal}` : ` with code ${code}`}`; + if (rpc) rpc.exitError = new Error(message); + for (const acpId of pending.values()) error(acpId, -32000, message); + pending.clear(); + if (!rpc.ready) { + for (const resolve of rpc.readyWaiters) resolve(); + rpc.readyWaiters = []; + } + rpc = null; + }); + + const lines = createInterface({ input: child.stdout, terminal: false }); + lines.on("line", (line) => { + if (!line.trim()) return; + try { + handleRpcEvent(JSON.parse(line)); + } catch (err) { + process.stderr.write(`ggcoder-acp: ignored non-JSON RPC output: ${line}\n`); + } + }); + + return rpc; +} + +function waitReady(state, timeoutMs = 120_000) { + if (state.ready) return Promise.resolve(); + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("timeout waiting for GG Coder RPC ready")), timeoutMs); + state.readyWaiters.push(() => { + clearTimeout(timeout); + if (state.ready) { + resolve(); + } else { + reject(state.exitError ?? new Error("GG Coder RPC exited before ready")); + } + }); + }); +} + +function sendRpc(command) { + if (!rpc?.child?.stdin?.writable) { + throw new Error("GG Coder RPC is not running"); + } + const id = nextRpcId++; + rpc.child.stdin.write(`${JSON.stringify({ id, ...command })}\n`); + return id; +} + +function waitForPending(timeoutMs = 5_000) { + if (pending.size === 0) return Promise.resolve(); + return new Promise((resolve) => { + const started = Date.now(); + const interval = setInterval(() => { + if (pending.size === 0 || Date.now() - started >= timeoutMs) { + clearInterval(interval); + resolve(); + } + }, 25); + }); +} + +async function handleAcp(message) { + const { id, method, params } = message; + + if (method === "initialize") { + result(id, { + protocolVersion: 1, + agentInfo: { name: "ggcoder", version: "ggcoder-acp" }, + agentCapabilities: { loadSession: false }, + }); + return; + } + + if (method === "session/new") { + sessionId = `ggcoder-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + const cwd = params?.cwd ?? process.cwd(); + const state = startRpc(cwd); + await waitReady(state); + result(id, { sessionId }); + return; + } + + if (method === "session/prompt") { + if (!sessionId) throw new Error("session/new must be called before session/prompt"); + const text = flattenPrompt(params?.prompt); + const rpcId = sendRpc({ command: "prompt", text }); + pending.set(rpcId, id); + return; + } + + if (method === "session/cancel") { + if (rpc?.child?.stdin?.writable) { + sendRpc({ command: "abort" }); + } + return; + } + + if (method === "session/load") { + error(id, -32601, "session/load is not supported by ggcoder-acp"); + return; + } + + if (method === "session/set_config_option") { + error(id, -32601, "session/set_config_option is not supported by ggcoder-acp"); + return; + } + + error(id, -32601, `Unsupported method: ${method}`); +} + +const acpLines = createInterface({ input: process.stdin, terminal: false }); +for await (const line of acpLines) { + if (!line.trim()) continue; + let message; + try { + message = JSON.parse(line); + } catch { + continue; + } + + try { + await handleAcp(message); + } catch (err) { + if (message.id !== undefined) { + error(message.id, -32000, err instanceof Error ? err.message : String(err)); + } + } +} + +await waitForPending(); +if (rpc?.child) rpc.child.kill("SIGTERM"); diff --git a/charts/openab/templates/NOTES.txt b/charts/openab/templates/NOTES.txt index a66175f3..45c4f939 100644 --- a/charts/openab/templates/NOTES.txt +++ b/charts/openab/templates/NOTES.txt @@ -36,7 +36,7 @@ Agents deployed: {{- else if eq (toString $cfg.command) "gemini" }} Authenticate: kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- gemini -{{- else if eq (toString $cfg.command) "ggcoder" }} +{{- else if eq (toString $cfg.command) "ggcoder-acp" }} Authenticate: kubectl exec -it deployment/{{ include "openab.agentFullname" (dict "ctx" $ "agent" $name) }} -- ggcoder login {{- else if eq (toString $cfg.command) "opencode" }} diff --git a/charts/openab/values.yaml b/charts/openab/values.yaml index 63d1c5c0..3725cbf0 100644 --- a/charts/openab/values.yaml +++ b/charts/openab/values.yaml @@ -110,9 +110,8 @@ agents: # resources: {} # image: "ghcr.io/openabdev/openab-opencode:latest" # ggcoder: - # command: ggcoder - # args: - # - --rpc + # command: ggcoder-acp + # args: [] # discord: # enabled: true # allowedChannels: diff --git a/docs/ggcoder.md b/docs/ggcoder.md index f03de6ce..6f2e3784 100644 --- a/docs/ggcoder.md +++ b/docs/ggcoder.md @@ -1,6 +1,8 @@ # GG Coder CLI -GG Coder supports ACP via the `--rpc` flag (JSON-RPC mode for IDE integrations). +GG Coder does not currently expose ACP directly. OpenAB's GG Coder image installs +`ggcoder-acp`, a small wrapper that speaks ACP on stdio and translates requests +to GG Coder's `--rpc` JSON-RPC mode. ## Docker Image @@ -8,7 +10,8 @@ GG Coder supports ACP via the `--rpc` flag (JSON-RPC mode for IDE integrations). docker build -f Dockerfile.ggcoder -t openab-ggcoder:latest . ``` -The image installs `@kenkaiiii/ggcoder` globally via npm. +The image installs `@kenkaiiii/ggcoder` globally via npm and copies the +`ggcoder-acp` wrapper into `/usr/local/bin`. ## Helm Install @@ -19,21 +22,21 @@ helm install openab openab/openab \ --set agents.ggcoder.discord.botToken="$DISCORD_BOT_TOKEN" \ --set-string 'agents.ggcoder.discord.allowedChannels[0]=YOUR_CHANNEL_ID' \ --set agents.ggcoder.image=ghcr.io/joseph19820124/openab-ggcoder:latest \ - --set agents.ggcoder.command=ggcoder \ - --set agents.ggcoder.args='{--rpc}' \ + --set agents.ggcoder.command=ggcoder-acp \ --set agents.ggcoder.workingDir=/home/node ``` > Set `agents.kiro.enabled=false` to disable the default Kiro agent. > -> (Optional) Use `--set agents.ggcoder.args='{--provider,anthropic,--rpc}'` to specify a provider explicitly. +> (Optional) Use `--set agents.ggcoder.args='{--provider,openai}'` to pass +> GG Coder CLI options through the ACP wrapper. ## Manual config.toml ```toml [agent] -command = "ggcoder" -args = ["--rpc"] +command = "ggcoder-acp" +args = [] working_dir = "/home/node" ``` diff --git a/src/setup/config.rs b/src/setup/config.rs index c0e7d604..d96d09cd 100644 --- a/src/setup/config.rs +++ b/src/setup/config.rs @@ -89,6 +89,7 @@ pub fn generate_config( "claude" => ("claude-agent-acp", vec![]), "codex" => ("codex-acp", vec![]), "gemini" => ("gemini", vec!["--acp".into()]), + "ggcoder" => ("ggcoder-acp", vec![]), other => (other, vec![]), }; AgentConfigToml { diff --git a/src/setup/validate.rs b/src/setup/validate.rs index 527a1a38..313e0f8b 100644 --- a/src/setup/validate.rs +++ b/src/setup/validate.rs @@ -24,7 +24,7 @@ pub fn validate_bot_token(token: &str) -> anyhow::Result<()> { /// Validate agent command #[cfg(test)] pub fn validate_agent_command(cmd: &str) -> anyhow::Result<()> { - let valid = ["kiro", "claude", "codex", "gemini"]; + let valid = ["kiro", "claude", "codex", "gemini", "ggcoder"]; if !valid.contains(&cmd) { anyhow::bail!("Agent must be one of: {}", valid.join(", ")); } @@ -63,7 +63,7 @@ mod tests { #[test] fn test_validate_agent_command() { - for agent in &["kiro", "claude", "codex", "gemini"] { + for agent in &["kiro", "claude", "codex", "gemini", "ggcoder"] { assert!(validate_agent_command(agent).is_ok()); } assert!(validate_agent_command("invalid").is_err()); diff --git a/src/setup/wizard.rs b/src/setup/wizard.rs index f5a78960..c9c3318e 100644 --- a/src/setup/wizard.rs +++ b/src/setup/wizard.rs @@ -383,12 +383,13 @@ fn section_agent() -> (String, String, bool) { "kiro: npm install -g @koryhutchison/kiro-cli", "codex: npm install -g openai-codex (requires OpenAI API key)", "gemini: npm install -g @google/gemini-cli", + "ggcoder: npm install -g @kenkaiiii/ggcoder + install ggcoder-acp", "", "Make sure the agent is in your PATH before continuing.", ]); println!(); - let choices = ["claude", "kiro", "codex", "gemini"]; + let choices = ["claude", "kiro", "codex", "gemini", "ggcoder"]; let idx = prompt_choice(" Select agent:", &choices); let agent = choices[idx]; @@ -530,6 +531,13 @@ fn print_next_steps(agent: &str, output_path: &Path, is_local: bool) { " 2. Authenticate via Google OAuth, or set GEMINI_API_KEY in config.toml" ); } + "ggcoder" => { + cprintln!(C.cyan, " 1. Install GG Coder CLI and the ACP wrapper:"); + println!(" npm install -g @kenkaiiii/ggcoder"); + println!(" install -m 755 bin/ggcoder-acp.mjs /usr/local/bin/ggcoder-acp"); + cprintln!(C.cyan, " 2. Authenticate:"); + println!(" ggcoder login"); + } _ => {} } @@ -566,6 +574,9 @@ fn print_next_steps(agent: &str, output_path: &Path, is_local: bool) { "gemini" => { println!(" Set GEMINI_API_KEY via secret, or exec into the pod for OAuth") } + "ggcoder" => { + println!(" kubectl exec -it deployment/openab-ggcoder -- ggcoder login") + } _ => {} } println!();