From 155e2b0daf8b16dc00bb6c8a9ca352743c4ea6ca Mon Sep 17 00:00:00 2001 From: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:58:23 +0800 Subject: [PATCH 1/5] feat: add startup config for copilot, codebuddy, and qwen providers The daemon adapters, protocol modules, CLI scripts (hask/bask/qask), and ask dispatcher were already merged but the ccb launcher was missing: - _ALLOWED_PROVIDERS whitelist - Provider validation (CCB_CALLER) - Unified askd daemon startup loop - Daemon spec mapping - Tmux/WezTerm pane creation dispatch - Warmup/ping routing - Start command generation - Session file writing - Claude env overrides for inter-provider communication - Help text listing - Legacy session migration Adds generic _start_generic_tmux(), _build_generic_start_cmd(), and _write_generic_session() methods to avoid duplicating the established pattern for each new pane-log provider. All 268 tests pass. Closes bfly123/claude_code_bridge#59 (follow-up) Ref bfly123/claude_code_bridge#122 --- ccb | 166 ++++++++++++++++++++++++++++++++++++---- lib/ccb_start_config.py | 2 +- 2 files changed, 152 insertions(+), 16 deletions(-) diff --git a/ccb b/ccb index 90d028c2..29c88dd0 100755 --- a/ccb +++ b/ccb @@ -40,7 +40,7 @@ from session_utils import ( ) from pane_registry import upsert_registry, load_registry_by_project_id from project_id import compute_ccb_project_id -from providers import CASK_CLIENT_SPEC, GASK_CLIENT_SPEC, OASK_CLIENT_SPEC, LASK_CLIENT_SPEC, DASK_CLIENT_SPEC +from providers import CASK_CLIENT_SPEC, GASK_CLIENT_SPEC, OASK_CLIENT_SPEC, LASK_CLIENT_SPEC, DASK_CLIENT_SPEC, HASK_CLIENT_SPEC, BASK_CLIENT_SPEC, QASK_CLIENT_SPEC from process_lock import ProviderLock from askd_rpc import shutdown_daemon, read_state from askd_runtime import state_file_path @@ -608,7 +608,7 @@ class AILauncher: """Managed env + explicit caller marker for the pane/provider process.""" env = self._managed_env_overrides() prov = (provider or "").strip().lower() - if prov in {"claude", "codex", "gemini", "opencode", "droid", "email", "manual"}: + if prov in {"claude", "codex", "gemini", "opencode", "droid", "copilot", "codebuddy", "qwen", "email", "manual"}: env["CCB_CALLER"] = prov return env @@ -631,7 +631,7 @@ class AILauncher: if not cfg.is_dir(): return - for name in (".codex-session", ".gemini-session", ".opencode-session", ".claude-session", ".droid-session"): + for name in (".codex-session", ".gemini-session", ".opencode-session", ".claude-session", ".droid-session", ".copilot-session", ".codebuddy-session", ".qwen-session"): legacy = self.project_root / name if not legacy.exists(): continue @@ -760,7 +760,7 @@ class AILauncher: def _maybe_start_unified_askd(self, *, quiet: bool = False) -> None: """Start unified askd daemon (provider-agnostic).""" # Try to start for any enabled provider that uses askd (including claude) - for provider in ["codex", "gemini", "opencode", "droid", "claude"]: + for provider in ["codex", "gemini", "opencode", "droid", "claude", "copilot", "codebuddy", "qwen"]: if provider in [p.lower() for p in self.providers]: # Try to start and check if successful self._maybe_start_provider_daemon(provider, quiet=quiet) @@ -799,6 +799,9 @@ class AILauncher: "opencode": OASK_CLIENT_SPEC, "claude": LASK_CLIENT_SPEC, "droid": DASK_CLIENT_SPEC, + "copilot": HASK_CLIENT_SPEC, + "codebuddy": BASK_CLIENT_SPEC, + "qwen": QASK_CLIENT_SPEC, } spec = specs.get(provider) if not spec: @@ -1227,6 +1230,8 @@ class AILauncher: return self._start_opencode_tmux(parent_pane=parent_pane, direction=direction) elif provider == "droid": return self._start_droid_tmux(parent_pane=parent_pane, direction=direction) + elif provider in ("copilot", "codebuddy", "qwen"): + return self._start_generic_tmux(provider, parent_pane=parent_pane, direction=direction) else: print(f"❌ {t('unknown_provider', provider=provider)}") return None @@ -1280,6 +1285,8 @@ class AILauncher: self._write_opencode_session(runtime, None, pane_id=pane_id, pane_title_marker=pane_title_marker, start_cmd=start_cmd) elif provider == "droid": self._write_droid_session(runtime, None, pane_id=pane_id, pane_title_marker=pane_title_marker, start_cmd=start_cmd) + elif provider in ("copilot", "codebuddy", "qwen"): + self._write_generic_session(provider, runtime, None, pane_id=pane_id, pane_title_marker=pane_title_marker, start_cmd=start_cmd) else: print(f"❌ {t('unknown_provider', provider=provider)}") return None @@ -1851,16 +1858,19 @@ class AILauncher: def _warmup_provider(self, provider: str, timeout: float = 8.0) -> bool: if provider == "gemini": return True - if provider == "codex": - ping_script = self.script_dir / "bin" / "cping" - elif provider == "gemini": - ping_script = self.script_dir / "bin" / "gping" - elif provider == "opencode": - ping_script = self.script_dir / "bin" / "oping" - elif provider == "droid": - ping_script = self.script_dir / "bin" / "dping" - else: + ping_map = { + "codex": "cping", + "gemini": "gping", + "opencode": "oping", + "droid": "dping", + "copilot": "hping", + "codebuddy": "bping", + "qwen": "qping", + } + ping_name = ping_map.get(provider) + if not ping_name: return False + ping_script = self.script_dir / "bin" / ping_name if not ping_script.exists(): return False @@ -1904,6 +1914,12 @@ class AILauncher: return self._build_opencode_start_cmd() elif provider == "droid": return self._build_droid_start_cmd() + elif provider == "copilot": + return self._build_generic_start_cmd(provider, "gh copilot", "COPILOT_START_CMD") + elif provider == "codebuddy": + return self._build_generic_start_cmd(provider, "codebuddy", "CODEBUDDY_START_CMD") + elif provider == "qwen": + return self._build_generic_start_cmd(provider, "qwen", "QWEN_START_CMD") return "" def _opencode_resume_allowed(self) -> bool: @@ -2347,6 +2363,113 @@ class AILauncher: print(f"✅ {t('started_backend', provider='Droid', terminal='tmux pane', pane_id=pane_id)}") return pane_id + def _start_generic_tmux( + self, + provider: str, + *, + parent_pane: str | None = None, + direction: str | None = None, + ) -> str | None: + runtime = self.runtime_dir / provider + runtime.mkdir(parents=True, exist_ok=True) + + env_overrides = self._provider_env_overrides(provider) + start_cmd = self._build_env_prefix(env_overrides) + _build_export_path_cmd(self.script_dir / "bin") + self._get_start_cmd(provider) + pane_title_marker = f"CCB-{provider.capitalize()}" + + backend = TmuxBackend() + + use_direction = (direction or ("right" if not self.tmux_panes else "bottom")).strip() or "right" + use_parent = parent_pane + if not use_parent: + try: + use_parent = backend.get_current_pane_id() + except Exception: + use_parent = None + if not use_parent and use_direction == "bottom": + try: + use_parent = next(reversed(self.tmux_panes.values())) + except StopIteration: + use_parent = None + + try: + if use_parent and str(use_parent).startswith("%") and not backend.pane_exists(str(use_parent)): + use_parent = backend.get_current_pane_id() + except Exception: + use_parent = None + + pane_id = backend.create_pane("", str(Path.cwd()), direction=use_direction, percent=50, parent_pane=use_parent) + backend.respawn_pane(pane_id, cmd=start_cmd, cwd=str(Path.cwd()), remain_on_exit=True) + backend.set_pane_title(pane_id, pane_title_marker) + backend.set_pane_user_option(pane_id, "@ccb_agent", provider.capitalize()) + + self.tmux_panes[provider] = pane_id + + self._write_generic_session( + provider, + runtime, + None, + pane_id=pane_id, + pane_title_marker=pane_title_marker, + start_cmd=start_cmd, + ) + + print(f"✅ {t('started_backend', provider=provider.capitalize(), terminal='tmux pane', pane_id=pane_id)}") + return pane_id + + def _build_generic_start_cmd(self, provider: str, default_cmd: str, env_var: str) -> str: + cmd = (os.environ.get(env_var) or default_cmd).strip() or default_cmd + return cmd + + def _write_generic_session(self, provider, runtime, tmux_session, pane_id=None, pane_title_marker=None, start_cmd=None): + session_file = self._project_session_file(f".{provider}-session") + + writable, reason, fix = check_session_writable(session_file) + if not writable: + print(f"❌ Cannot write {session_file.name}: {reason}", file=sys.stderr) + print(f"💡 Fix: {fix}", file=sys.stderr) + return False + + data = { + "session_id": self.session_id, + "ccb_session_id": self.session_id, + "ccb_project_id": compute_ccb_project_id(self.project_root), + "runtime_dir": str(runtime), + "terminal": self.terminal_type, + "tmux_session": tmux_session, + "pane_id": pane_id, + "pane_title_marker": pane_title_marker, + "work_dir": str(self.project_root), + "work_dir_norm": _normalize_path_for_match(str(self.project_root)), + "start_dir": str(self.invocation_dir), + "active": True, + "started_at": time.strftime("%Y-%m-%d %H:%M:%S"), + "start_cmd": str(start_cmd) if start_cmd else None, + } + + ok, err = safe_write_session(session_file, json.dumps(data, ensure_ascii=False, indent=2)) + if not ok: + print(err, file=sys.stderr) + return False + try: + upsert_registry({ + "ccb_session_id": self.session_id, + "ccb_project_id": compute_ccb_project_id(self.project_root), + "work_dir": str(self.project_root), + "terminal": self.terminal_type, + "providers": { + provider: { + "pane_id": pane_id, + "pane_title_marker": pane_title_marker, + "session_file": str(session_file), + } + }, + }) + except Exception: + pass + self._maybe_start_provider_daemon(provider) + return True + def _start_cmd_pane( self, *, @@ -3068,6 +3191,19 @@ class AILauncher: else: env["DROID_TMUX_SESSION"] = pane_id + for extra in ("copilot", "codebuddy", "qwen"): + if extra in self.providers: + runtime = self.runtime_dir / extra + prefix = extra.upper() + env[f"{prefix}_SESSION_ID"] = self.session_id + env[f"{prefix}_RUNTIME_DIR"] = str(runtime) + env[f"{prefix}_TERMINAL"] = self.terminal_type or "" + pane_id = self._provider_pane_id(extra) + if self.terminal_type == "wezterm": + env[f"{prefix}_WEZTERM_PANE"] = pane_id + else: + env[f"{prefix}_TMUX_SESSION"] = pane_id + return env def _build_claude_env(self) -> dict: @@ -4847,7 +4983,7 @@ def main(): subparsers = parser.add_subparsers(dest="command", help="Subcommands") kill_parser = subparsers.add_parser("kill", help="Terminate session or clean up zombies") - kill_parser.add_argument("providers", nargs="*", default=[], help="Backends to terminate (codex/gemini/opencode/claude/droid)") + kill_parser.add_argument("providers", nargs="*", default=[], help="Backends to terminate (codex/gemini/opencode/claude/droid/copilot/codebuddy/qwen)") kill_parser.add_argument("-f", "--force", action="store_true", help="Clean up all zombie tmux sessions globally") kill_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt (with -f)") @@ -4885,7 +5021,7 @@ def main(): start_parser.add_argument( "providers", nargs="*", - help="Backends to start (space or comma separated): codex, gemini, opencode, claude, droid (add cmd for a shell pane)", + help="Backends to start (space or comma separated): codex, gemini, opencode, claude, droid, copilot, codebuddy, qwen (add cmd for a shell pane)", ) start_parser.add_argument("-r", "--resume", "--restore", action="store_true", help="Resume context") start_parser.add_argument("-a", "--auto", action="store_true", help="Full auto permission mode") diff --git a/lib/ccb_start_config.py b/lib/ccb_start_config.py index 58da17ae..6f369fb9 100644 --- a/lib/ccb_start_config.py +++ b/lib/ccb_start_config.py @@ -19,7 +19,7 @@ class StartConfig: path: Optional[Path] = None -_ALLOWED_PROVIDERS = {"codex", "gemini", "opencode", "claude", "droid"} +_ALLOWED_PROVIDERS = {"codex", "gemini", "opencode", "claude", "droid", "copilot", "codebuddy", "qwen"} def _parse_tokens(raw: str) -> list[str]: From 546be076c96675036a0f4092fe4b2bbfc71224f0 Mon Sep 17 00:00:00 2001 From: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:00:50 +0800 Subject: [PATCH 2/5] docs: add test screenshots for PR comments --- .github/screenshots/ccb_help_screenshot.png | Bin 0 -> 32370 bytes .github/screenshots/ccb_test_screenshot.png | Bin 0 -> 12707 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/screenshots/ccb_help_screenshot.png create mode 100644 .github/screenshots/ccb_test_screenshot.png diff --git a/.github/screenshots/ccb_help_screenshot.png b/.github/screenshots/ccb_help_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..580d21b77f03b87025f2229eef0ac44e0c5014a6 GIT binary patch literal 32370 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 A(EtDd literal 0 HcmV?d00001 diff --git a/.github/screenshots/ccb_test_screenshot.png b/.github/screenshots/ccb_test_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..876840032462a4d6ed7e02f6877dc46c815c2aa5 GIT binary patch literal 12707 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 literal 0 HcmV?d00001 From 14e69d917a9f53f5bafd45c9102344a9bb315fac Mon Sep 17 00:00:00 2001 From: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:22:51 +0800 Subject: [PATCH 3/5] fix: add new providers to CLI parser allowed sets _parse_providers() and _parse_providers_with_cmd() had their own hardcoded allowed sets that were not updated, causing `ccb copilot` to fail with "invalid provider(s)" even though the help text listed the new providers. Reported by @WenYuLuo in #139. --- ccb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ccb b/ccb index 29c88dd0..609c1b02 100755 --- a/ccb +++ b/ccb @@ -4094,7 +4094,7 @@ def _parse_providers(values: list[str], *, allow_unknown: bool = False) -> list[ Returns a de-duplicated list preserving order. """ - allowed = {"codex", "gemini", "opencode", "claude", "droid"} + allowed = {"codex", "gemini", "opencode", "claude", "droid", "copilot", "codebuddy", "qwen"} raw_parts = _split_provider_tokens(values) if not raw_parts: @@ -4114,8 +4114,8 @@ def _parse_providers(values: list[str], *, allow_unknown: bool = False) -> list[ if unknown and not allow_unknown: print(f"❌ invalid provider(s): {', '.join(unknown)}", file=sys.stderr) - print("💡 use: ccb codex gemini opencode claude droid (spaces) or ccb codex,gemini,opencode,claude,droid (commas)", file=sys.stderr) - print("💡 allowed: codex, gemini, opencode, claude, droid", file=sys.stderr) + print("💡 use: ccb codex gemini opencode claude droid copilot codebuddy qwen (spaces) or ccb codex,gemini,opencode,claude,droid,copilot,codebuddy,qwen (commas)", file=sys.stderr) + print("💡 allowed: codex, gemini, opencode, claude, droid, copilot, codebuddy, qwen", file=sys.stderr) return [] return parsed @@ -4126,7 +4126,7 @@ def _parse_providers_with_cmd(values: list[str]) -> tuple[list[str], bool]: Parse providers from argv and treat "cmd" as a separate flag. Returns (providers, cmd_enabled). """ - allowed = {"codex", "gemini", "opencode", "claude", "droid"} + allowed = {"codex", "gemini", "opencode", "claude", "droid", "copilot", "codebuddy", "qwen"} raw_parts = _split_provider_tokens(values) if not raw_parts: return [], False @@ -4150,8 +4150,8 @@ def _parse_providers_with_cmd(values: list[str]) -> tuple[list[str], bool]: if unknown: print(f"❌ invalid provider(s): {', '.join(unknown)}", file=sys.stderr) - print("💡 use: ccb codex gemini opencode claude droid cmd (spaces) or ccb codex,gemini,opencode,claude,droid,cmd (commas)", file=sys.stderr) - print("💡 allowed: codex, gemini, opencode, claude, droid, cmd", file=sys.stderr) + print("💡 use: ccb codex gemini opencode claude droid copilot codebuddy qwen cmd (spaces) or ccb codex,gemini,opencode,claude,droid,copilot,codebuddy,qwen,cmd (commas)", file=sys.stderr) + print("💡 allowed: codex, gemini, opencode, claude, droid, copilot, codebuddy, qwen, cmd", file=sys.stderr) return [], cmd_enabled return parsed, cmd_enabled From f8243636d6ac827553845efed0aec4a15077ff98 Mon Sep 17 00:00:00 2001 From: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:16:45 +0800 Subject: [PATCH 4/5] fix: sync all provider whitelist/enum across entire codebase Update every hardcoded provider list to include copilot, codebuddy, and qwen (and droid where missing): - lib/ccb_start_config.py: DEFAULT_PROVIDERS - lib/pane_registry.py: legacy migration loops (2 locations) - lib/web/routes/providers.py: KNOWN_PROVIDERS - lib/memory/transfer.py: SUPPORTED_PROVIDERS, SUPPORTED_SOURCES, SOURCE_SESSION_FILES, DEFAULT_SOURCE_ORDER - lib/mail/config.py: SUPPORTED_PROVIDERS - lib/mail_tui/wizard.py: default_map (mail wizard choices) - mcp/ccb-delegation/server.py: PROVIDERS dict, ALIAS_TOOLS, TOOL_DEFS loop - bin/ccb-mounted: PROVIDERS Audited with two independent reviewers (grep-based + lifecycle trace) across three rounds to ensure zero omissions. All 268 tests pass. --- bin/ccb-mounted | 2 +- lib/ccb_start_config.py | 2 +- lib/mail/config.py | 2 +- lib/mail_tui/wizard.py | 7 +++++-- lib/memory/transfer.py | 9 ++++++--- lib/pane_registry.py | 4 ++-- lib/web/routes/providers.py | 2 +- mcp/ccb-delegation/server.py | 18 +++++++++++++++++- 8 files changed, 34 insertions(+), 12 deletions(-) diff --git a/bin/ccb-mounted b/bin/ccb-mounted index 5156cce3..e6fa3a1c 100755 --- a/bin/ccb-mounted +++ b/bin/ccb-mounted @@ -4,7 +4,7 @@ set -euo pipefail -PROVIDERS="codex:cask gemini:gask opencode:oask claude:lask droid:dask" +PROVIDERS="codex:cask gemini:gask opencode:oask claude:lask droid:dask copilot:hask codebuddy:bask qwen:qask" CWD=$(pwd) FORMAT="--json" AUTOSTART=false diff --git a/lib/ccb_start_config.py b/lib/ccb_start_config.py index 6f369fb9..efd5e77d 100644 --- a/lib/ccb_start_config.py +++ b/lib/ccb_start_config.py @@ -10,7 +10,7 @@ CONFIG_FILENAME = "ccb.config" -DEFAULT_PROVIDERS = ["codex", "gemini", "opencode", "claude"] +DEFAULT_PROVIDERS = ["codex", "gemini", "opencode", "claude", "droid", "copilot", "codebuddy", "qwen"] @dataclass diff --git a/lib/mail/config.py b/lib/mail/config.py index 380feba5..b4a71371 100644 --- a/lib/mail/config.py +++ b/lib/mail/config.py @@ -32,7 +32,7 @@ CURRENT_CONFIG_VERSION = 3 # Supported AI providers -SUPPORTED_PROVIDERS = ["claude", "codex", "gemini", "opencode", "droid"] +SUPPORTED_PROVIDERS = ["claude", "codex", "gemini", "opencode", "droid", "copilot", "codebuddy", "qwen"] # Notification modes NotifyMode = Literal["on_completion", "realtime", "periodic", "on_request"] diff --git a/lib/mail_tui/wizard.py b/lib/mail_tui/wizard.py index 7d20f81d..a0511c58 100644 --- a/lib/mail_tui/wizard.py +++ b/lib/mail_tui/wizard.py @@ -104,9 +104,12 @@ def run_simple_wizard() -> bool: print(" 3. Gemini") print(" 4. OpenCode") print(" 5. Droid") + print(" 6. Copilot") + print(" 7. CodeBuddy") + print(" 8. Qwen") - default_choice = input("\nEnter choice [1-5]: ").strip() - default_map = {"1": "claude", "2": "codex", "3": "gemini", "4": "opencode", "5": "droid"} + default_choice = input("\nEnter choice [1-8]: ").strip() + default_map = {"1": "claude", "2": "codex", "3": "gemini", "4": "opencode", "5": "droid", "6": "copilot", "7": "codebuddy", "8": "qwen"} default_provider = default_map.get(default_choice, "claude") # Step 6: Allowed senders (whitelist) diff --git a/lib/memory/transfer.py b/lib/memory/transfer.py index 063d4ab7..69886b30 100644 --- a/lib/memory/transfer.py +++ b/lib/memory/transfer.py @@ -23,16 +23,19 @@ class ContextTransfer: """Orchestrate context transfer between providers.""" - SUPPORTED_PROVIDERS = ("codex", "gemini", "opencode", "droid") - SUPPORTED_SOURCES = ("auto", "claude", "codex", "gemini", "opencode", "droid") + SUPPORTED_PROVIDERS = ("codex", "gemini", "opencode", "droid", "copilot", "codebuddy", "qwen") + SUPPORTED_SOURCES = ("auto", "claude", "codex", "gemini", "opencode", "droid", "copilot", "codebuddy", "qwen") SOURCE_SESSION_FILES = { "claude": ".claude-session", "codex": ".codex-session", "gemini": ".gemini-session", "opencode": ".opencode-session", "droid": ".droid-session", + "copilot": ".copilot-session", + "codebuddy": ".codebuddy-session", + "qwen": ".qwen-session", } - DEFAULT_SOURCE_ORDER = ("claude", "codex", "gemini", "opencode", "droid") + DEFAULT_SOURCE_ORDER = ("claude", "codex", "gemini", "opencode", "droid", "copilot", "codebuddy", "qwen") DEFAULT_FALLBACK_PAIRS = 50 def __init__( diff --git a/lib/pane_registry.py b/lib/pane_registry.py index 3ace8b8d..6b4659e2 100644 --- a/lib/pane_registry.py +++ b/lib/pane_registry.py @@ -132,7 +132,7 @@ def _get_providers_map(data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]: # Legacy flat format: derive providers on demand (no persistence here). out = {} - for p in ("codex", "gemini", "opencode", "claude"): + for p in ("codex", "gemini", "opencode", "claude", "droid", "copilot", "codebuddy", "qwen"): entry = _provider_entry_from_legacy(data, p) if entry: out[p] = entry @@ -327,7 +327,7 @@ def upsert_registry(record: Dict[str, Any]) -> bool: providers[p][k] = v # Migrate legacy flat fields into providers. - for p in ("codex", "gemini", "opencode", "claude"): + for p in ("codex", "gemini", "opencode", "claude", "droid", "copilot", "codebuddy", "qwen"): legacy_entry = _provider_entry_from_legacy(record, p) if legacy_entry: providers.setdefault(p, {}) diff --git a/lib/web/routes/providers.py b/lib/web/routes/providers.py index b6acc39a..95afab62 100644 --- a/lib/web/routes/providers.py +++ b/lib/web/routes/providers.py @@ -28,7 +28,7 @@ class PingResult(BaseModel): error: Optional[str] = None -KNOWN_PROVIDERS = ["claude", "codex", "gemini", "opencode", "droid"] +KNOWN_PROVIDERS = ["claude", "codex", "gemini", "opencode", "droid", "copilot", "codebuddy", "qwen"] def check_provider_available(provider: str) -> ProviderStatus: diff --git a/mcp/ccb-delegation/server.py b/mcp/ccb-delegation/server.py index e14ed45e..230dbac4 100644 --- a/mcp/ccb-delegation/server.py +++ b/mcp/ccb-delegation/server.py @@ -33,6 +33,10 @@ "gemini": {"ask": "gask", "pend": "gpend", "ping": "gping"}, "claude": {"ask": "lask", "pend": "lpend", "ping": "lping"}, "opencode": {"ask": "oask", "pend": "opend", "ping": "oping"}, + "droid": {"ask": "dask", "pend": "dpend", "ping": "dping"}, + "copilot": {"ask": "hask", "pend": "hpend", "ping": "hping"}, + "codebuddy": {"ask": "bask", "pend": "bpend", "ping": "bping"}, + "qwen": {"ask": "qask", "pend": "qpend", "ping": "qping"}, } ALIAS_TOOLS = [ @@ -40,14 +44,26 @@ ("gask", "gemini", "ask"), ("lask", "claude", "ask"), ("oask", "opencode", "ask"), + ("dask", "droid", "ask"), + ("hask", "copilot", "ask"), + ("bask", "codebuddy", "ask"), + ("qask", "qwen", "ask"), ("cpend", "codex", "pend"), ("gpend", "gemini", "pend"), ("lpend", "claude", "pend"), ("opend", "opencode", "pend"), + ("dpend", "droid", "pend"), + ("hpend", "copilot", "pend"), + ("bpend", "codebuddy", "pend"), + ("qpend", "qwen", "pend"), ("cping", "codex", "ping"), ("gping", "gemini", "ping"), ("lping", "claude", "ping"), ("oping", "opencode", "ping"), + ("dping", "droid", "ping"), + ("hping", "copilot", "ping"), + ("bping", "codebuddy", "ping"), + ("qping", "qwen", "ping"), ] ALIAS_MAP = {name: (provider, kind) for name, provider, kind in ALIAS_TOOLS} @@ -105,7 +121,7 @@ def _ping_schema() -> dict[str, Any]: TOOL_DEFS = [] -for provider in ("codex", "gemini", "claude", "opencode"): +for provider in ("codex", "gemini", "claude", "opencode", "droid", "copilot", "codebuddy", "qwen"): TOOL_DEFS.append( { "name": f"ccb_ask_{provider}", From 8a74ca9f1212cda3397007946e50bb1fc040c559 Mon Sep 17 00:00:00 2001 From: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com> Date: Fri, 13 Mar 2026 18:17:10 +0800 Subject: [PATCH 5/5] docs: update test screenshot after full provider sync --- .github/screenshots/ccb_test_final.png | Bin 0 -> 9623 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/screenshots/ccb_test_final.png diff --git a/.github/screenshots/ccb_test_final.png b/.github/screenshots/ccb_test_final.png new file mode 100644 index 0000000000000000000000000000000000000000..4ad1f27e26592af64c56d771cd74cd9cd481dd55 GIT binary patch literal 9623 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 I+u z$da=V0`bP@Jbubj;6Y?1(p1RH-tGjypT){xljJy3UVzjo++J^EU3Q1b=3u@oMIz@+ zN%Y5_y*yYLg5>kgJ0_egs|@`!MJOBNqLLl9J=`PT3SF8RFeCM?*9q+U?3>{Lgv_7$ zf!fX&#Enu#^^QoP`EXl~7pKdyaP68IH#T_xsQ(IM!D#->GMp1)*fq8h(||O|x8zei zBpgzcRU9jE1Ls+TV(N*bx!D+t0nfx(JH=OIvv2^=!Otph#vXEXC>tHHZACg1LWA z>bqcQpIghQXIqc+UQ*la!#o_?=O@Ctt)jZ6hJoWU$@Bg=*hgWmI@1E*`u%FnH#%px zUE2!CZ*|?~k)PsSTpUGKBF~|%Q%qb*^*R@Nrmk-DOPCK%7Ue|M`;Ia=muA!@g~7H~ z&WSQ)ZW8r(Z(MBg>QVE4y0=}S9bvw)e@#{#fODX@9+&jS5N%z;DPgP#8J6#Hobj?OphD6yN2GYX2s%1kus{HyAD3J8P@^r3 z##hgVkTD_}%ntd2aUp0JLoZ*sym)qYcr8OW#U5FJ?9=vK-^Q!2t*^cZ^fx@ec~$j{ zK&s`OdqzGvKLy=Rz9B6e=YAam>@3$xMeM0rg>&NT_=SzoAtQko&!U#ASk6&3DLJX) zxso+QwB1>o&o=56%nk&vs!GYV5WC|>h>ArP6$yT*pPxSi-oi5btc}%^L0Hd?Wev5W z-4msVia*Q8@SQzsUiYf<^>aJ;nbS4N{^m|zuCie|xB}1LGMuX!xH;S&yNHA&_26n9 z4mPt|Nm#H-(pQXMXi35=L@+>MeY=JH zLCTqERqua9;!lp2FVv{=hL#b^G! zakaC?Ww^G+G;4ONQVl9a-g1>LE{CuASGcM^+wYU~mI#dN9>W<>0%p?6)|C{={wYbgku^bocH`>LeG~~z^WK3ZkYOhY4gb?IUcfLm=O>Z-z58!Yv%Dj~! z`&(5S#OwC0CTIjEaDIOJ=rwUlN`6g!)mLyM5S1HTbZqBT2R{Jb7kgi}o1@kc>%Bhu zeM8{ z^Z9~oXm#u#GK5{y;$`OOszC2DN0B_+b5uupBI40!uQSd?eh``$PWbFRMdw`{oFytE zHX#;TI=l>&M%ur>8QYyc07Bxw@&BKHDd_*dn{-A?U-q^n;J5l;y)-Hf6