Skip to content

Commit 2b62336

Browse files
authored
Merge pull request SeemSeam#123 from LeoLin990405/feat/codebuddy-cli-provider
feat: add Tencent CodeBuddy CLI as new provider
2 parents 36e6f7b + 10d0750 commit 2b62336

17 files changed

Lines changed: 1977 additions & 5 deletions

bin/ask

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Usage:
66
ask <provider> [options] <message>
77
88
Providers:
9-
gemini, codex, opencode, droid, claude, copilot
9+
gemini, codex, opencode, droid, claude, copilot, codebuddy
1010
1111
Modes:
1212
Default (async): Background task with hook callback
@@ -57,6 +57,7 @@ PROVIDER_DAEMONS = {
5757
"droid": "dask",
5858
"claude": "lask",
5959
"copilot": "hask",
60+
"codebuddy": "bask",
6061
}
6162

6263
CALLER_SESSION_FILES = {
@@ -66,6 +67,7 @@ CALLER_SESSION_FILES = {
6667
"opencode": ".opencode-session",
6768
"droid": ".droid-session",
6869
"copilot": ".copilot-session",
70+
"codebuddy": ".codebuddy-session",
6971
}
7072

7173
CALLER_PANE_ENV_HINTS = {
@@ -74,6 +76,7 @@ CALLER_PANE_ENV_HINTS = {
7476
"opencode": ("OPENCODE_TMUX_SESSION", "OPENCODE_WEZTERM_PANE"),
7577
"droid": ("DROID_TMUX_SESSION", "DROID_WEZTERM_PANE"),
7678
"copilot": ("COPILOT_TMUX_SESSION", "COPILOT_WEZTERM_PANE"),
79+
"codebuddy": ("CODEBUDDY_TMUX_SESSION", "CODEBUDDY_WEZTERM_PANE"),
7780
}
7881

7982
CALLER_ENV_HINTS = {
@@ -82,6 +85,7 @@ CALLER_ENV_HINTS = {
8285
"opencode": ("OPENCODE_SESSION_ID", "OPENCODE_RUNTIME_DIR"),
8386
"droid": ("DROID_SESSION_ID", "DROID_RUNTIME_DIR"),
8487
"copilot": ("COPILOT_SESSION_ID", "COPILOT_RUNTIME_DIR"),
88+
"codebuddy": ("CODEBUDDY_SESSION_ID", "CODEBUDDY_RUNTIME_DIR"),
8589
}
8690

8791
VALID_CALLERS = set(CALLER_SESSION_FILES.keys()) | {"email", "manual"}
@@ -475,7 +479,7 @@ def _usage() -> None:
475479
print("Usage: ask <provider> [options] <message>", file=sys.stderr)
476480
print("", file=sys.stderr)
477481
print("Providers:", file=sys.stderr)
478-
print(" gemini, codex, opencode, droid, claude, copilot", file=sys.stderr)
482+
print(" gemini, codex, opencode, droid, claude, copilot, codebuddy", file=sys.stderr)
479483
print("", file=sys.stderr)
480484
print("Options:", file=sys.stderr)
481485
print(" -h, --help Show this help message", file=sys.stderr)

bin/askd

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""
33
askd - Unified Ask Daemon for all AI providers.
44
5-
Single daemon process handling codex, gemini, opencode, droid, claude, and copilot.
5+
Single daemon process handling codex, gemini, opencode, droid, claude, copilot, and codebuddy.
66
"""
77
from __future__ import annotations
88

@@ -27,6 +27,7 @@ from askd.adapters.opencode import OpenCodeAdapter
2727
from askd.adapters.droid import DroidAdapter
2828
from askd.adapters.claude import ClaudeAdapter
2929
from askd.adapters.copilot import CopilotAdapter
30+
from askd.adapters.codebuddy import CodebuddyAdapter
3031

3132

3233
def _parse_listen(value: str) -> tuple[str, int]:
@@ -39,7 +40,7 @@ def _parse_listen(value: str) -> tuple[str, int]:
3940
return host or "127.0.0.1", int(port_s or "0")
4041

4142

42-
ALL_PROVIDERS = ["codex", "gemini", "opencode", "droid", "claude", "copilot"]
43+
ALL_PROVIDERS = ["codex", "gemini", "opencode", "droid", "claude", "copilot", "codebuddy"]
4344

4445
ADAPTER_CLASSES = {
4546
"codex": CodexAdapter,
@@ -48,6 +49,7 @@ ADAPTER_CLASSES = {
4849
"droid": DroidAdapter,
4950
"claude": ClaudeAdapter,
5051
"copilot": CopilotAdapter,
52+
"codebuddy": CodebuddyAdapter,
5153
}
5254

5355

bin/bask

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env python3
2+
"""
3+
bask - Send message to CodeBuddy and wait for reply (sync).
4+
5+
Designed to be used with Claude Code's run_in_background=true.
6+
If --output is provided, reply is written atomically to that file and stdout stays empty.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import os
12+
import sys
13+
import time
14+
from pathlib import Path
15+
16+
script_dir = Path(__file__).resolve().parent
17+
lib_dir = script_dir.parent / "lib"
18+
sys.path.insert(0, str(lib_dir))
19+
from compat import read_stdin_text, setup_windows_encoding
20+
21+
setup_windows_encoding()
22+
23+
from cli_output import EXIT_ERROR, EXIT_NO_REPLY, EXIT_OK, atomic_write_text
24+
from env_utils import env_bool
25+
from askd_client import (
26+
state_file_from_env,
27+
find_project_session_file,
28+
resolve_work_dir_with_registry,
29+
try_daemon_request,
30+
maybe_start_daemon,
31+
wait_for_daemon_ready,
32+
check_background_mode,
33+
)
34+
from providers import BASK_CLIENT_SPEC
35+
36+
37+
ASYNC_GUARDRAIL = """[CCB_ASYNC_SUBMITTED provider=codebuddy]
38+
IMPORTANT: Task submitted to CodeBuddy. You MUST:
39+
1. Tell user "CodeBuddy processing..."
40+
2. END YOUR TURN IMMEDIATELY
41+
3. Do NOT wait, poll, check status, or use any more tools
42+
"""
43+
44+
45+
def _daemon_startup_wait_s(timeout: float) -> float:
46+
raw = (os.environ.get("CCB_BASKD_STARTUP_WAIT_S") or "").strip()
47+
if raw:
48+
try:
49+
v = float(raw)
50+
except Exception:
51+
v = 0.0
52+
if v > 0:
53+
return min(max(0.2, v), max(0.2, float(timeout)))
54+
return min(8.0, max(1.0, float(timeout)))
55+
56+
57+
def _daemon_retry_wait_s(timeout: float) -> float:
58+
raw = (os.environ.get("CCB_BASKD_RETRY_WAIT_S") or "").strip()
59+
if raw:
60+
try:
61+
v = float(raw)
62+
except Exception:
63+
v = 0.0
64+
if v > 0:
65+
return min(1.0, max(0.05, v))
66+
return min(0.3, max(0.05, float(timeout) / 50.0))
67+
68+
69+
def _daemon_request_with_retries(
70+
work_dir: Path,
71+
message: str,
72+
timeout: float,
73+
quiet: bool,
74+
output_path: Path | None,
75+
) -> tuple[str, int] | None:
76+
state_file = state_file_from_env(BASK_CLIENT_SPEC.state_file_env)
77+
78+
result = try_daemon_request(BASK_CLIENT_SPEC, work_dir, message, timeout, quiet, state_file, output_path=output_path)
79+
if result is not None:
80+
return result
81+
82+
if not env_bool(BASK_CLIENT_SPEC.enabled_env, True):
83+
return None
84+
if not find_project_session_file(work_dir, BASK_CLIENT_SPEC.session_filename):
85+
return None
86+
87+
if state_file and state_file.exists():
88+
try:
89+
if not wait_for_daemon_ready(BASK_CLIENT_SPEC, 0.2, state_file):
90+
try:
91+
state_file.unlink()
92+
except Exception:
93+
pass
94+
except Exception:
95+
pass
96+
97+
started = maybe_start_daemon(BASK_CLIENT_SPEC, work_dir)
98+
if started:
99+
wait_for_daemon_ready(BASK_CLIENT_SPEC, _daemon_startup_wait_s(timeout), state_file)
100+
101+
wait_s = _daemon_retry_wait_s(timeout)
102+
deadline = time.time() + min(3.0, max(0.2, float(timeout)))
103+
while time.time() < deadline:
104+
result = try_daemon_request(BASK_CLIENT_SPEC, work_dir, message, timeout, quiet, state_file, output_path=output_path)
105+
if result is not None:
106+
return result
107+
time.sleep(wait_s)
108+
109+
return None
110+
111+
112+
def _usage() -> None:
113+
print("Usage: bask [--sync] [--session-file FILE] [--timeout SECONDS] [--output FILE] <message>", file=sys.stderr)
114+
115+
116+
def main(argv: list[str]) -> int:
117+
if len(argv) <= 1 and sys.stdin.isatty():
118+
_usage()
119+
return EXIT_ERROR
120+
121+
output_path: Path | None = None
122+
timeout: float | None = None
123+
quiet = False
124+
sync_mode = False
125+
session_file: str | None = None
126+
127+
parts: list[str] = []
128+
it = iter(argv[1:])
129+
for token in it:
130+
if token in ("-h", "--help"):
131+
_usage()
132+
return EXIT_OK
133+
if token in ("-q", "--quiet"):
134+
quiet = True
135+
continue
136+
if token == "--sync":
137+
sync_mode = True
138+
continue
139+
if token == "--session-file":
140+
try:
141+
session_file = next(it)
142+
except StopIteration:
143+
print("[ERROR] --session-file requires a file path", file=sys.stderr)
144+
return EXIT_ERROR
145+
continue
146+
if token in ("-o", "--output"):
147+
try:
148+
output_path = Path(next(it)).expanduser()
149+
except StopIteration:
150+
print("[ERROR] --output requires a file path", file=sys.stderr)
151+
return EXIT_ERROR
152+
continue
153+
if token in ("-t", "--timeout"):
154+
try:
155+
timeout = float(next(it))
156+
except StopIteration:
157+
print("[ERROR] --timeout requires a number", file=sys.stderr)
158+
return EXIT_ERROR
159+
except ValueError:
160+
print("[ERROR] --timeout must be a number", file=sys.stderr)
161+
return EXIT_ERROR
162+
continue
163+
parts.append(token)
164+
165+
message = " ".join(parts).strip()
166+
if not message and not sys.stdin.isatty():
167+
message = read_stdin_text().strip()
168+
if not message:
169+
print("[ERROR] Message cannot be empty", file=sys.stderr)
170+
return EXIT_ERROR
171+
172+
if timeout is None:
173+
try:
174+
timeout = float(os.environ.get("CCB_SYNC_TIMEOUT", "3600.0"))
175+
except Exception:
176+
timeout = 3600.0
177+
178+
try:
179+
work_dir, _ = resolve_work_dir_with_registry(
180+
BASK_CLIENT_SPEC,
181+
provider="codebuddy",
182+
cli_session_file=session_file,
183+
env_session_file=os.environ.get("CCB_SESSION_FILE"),
184+
)
185+
186+
daemon_result = _daemon_request_with_retries(work_dir, message, timeout, quiet, output_path)
187+
if daemon_result is not None:
188+
reply, exit_code = daemon_result
189+
if not sync_mode:
190+
print(ASYNC_GUARDRAIL, file=sys.stderr, flush=True)
191+
if output_path:
192+
atomic_write_text(output_path, reply + "\n")
193+
return exit_code
194+
sys.stdout.write(reply)
195+
if not reply.endswith("\n"):
196+
sys.stdout.write("\n")
197+
return exit_code
198+
199+
if not env_bool(BASK_CLIENT_SPEC.enabled_env, True):
200+
print(f"[ERROR] {BASK_CLIENT_SPEC.enabled_env}=0: bask daemon mode disabled.", file=sys.stderr)
201+
return EXIT_ERROR
202+
if not find_project_session_file(work_dir, BASK_CLIENT_SPEC.session_filename):
203+
print("[ERROR] No active CodeBuddy session found for this directory.", file=sys.stderr)
204+
print("Run `ccb codebuddy` (or add codebuddy to ccb.config) in this project first.", file=sys.stderr)
205+
return EXIT_ERROR
206+
print("[ERROR] bask daemon required but not available.", file=sys.stderr)
207+
print("Start it with `askd` (or enable autostart via CCB_BASKD_AUTOSTART=1).", file=sys.stderr)
208+
return EXIT_ERROR
209+
except KeyboardInterrupt:
210+
return 130
211+
except Exception as exc:
212+
print(f"[ERROR] {exc}", file=sys.stderr)
213+
return EXIT_ERROR
214+
215+
216+
if __name__ == "__main__":
217+
sys.exit(main(sys.argv))

0 commit comments

Comments
 (0)