openab 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
openab-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
openab-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: openab
3
+ Version: 0.3.0
4
+ Summary: Open Agent Bridge — connect agent backends (CLIs, APIs) to chat platforms (Telegram, …).
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: python-telegram-bot>=21.0
10
+ Requires-Dist: discord.py>=2.3.0
11
+ Requires-Dist: typer>=0.12.0
12
+ Requires-Dist: python-dotenv>=1.0.0
13
+ Requires-Dist: pyyaml>=6.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: build; extra == "dev"
16
+ Dynamic: license-file
17
+
18
+ <p align="center">
19
+ <img src="assets/icon.png" width="256" alt="OpenAB" />
20
+ </p>
21
+
22
+ # OpenAB
23
+
24
+ **Open Agent Bridge** — Connect AI agents (Cursor, Codex, Gemini, Claude, OpenClaw) to chat platforms. One config, one bridge.
25
+
26
+ [中文](docs/README.zh-CN.md) · [Configuration & usage](docs/guide.md)
27
+
28
+ ---
29
+
30
+ ## What it does
31
+
32
+ OpenAB forwards messages from **chat platforms** to an agent backend you choose and sends the reply back. No per-agent bots — configure once and chat.
33
+
34
+ | Agents | Chats |
35
+ |--------|-------|
36
+ | Cursor, Codex, Gemini, Claude, OpenClaw | Telegram, Discord, _more planned_ |
37
+
38
+ ---
39
+
40
+ ## Quick start
41
+
42
+ **1. Install** (Python 3.10+)
43
+
44
+ ```bash
45
+ pip install openab
46
+ # or: uv tool install openab
47
+ ```
48
+
49
+ **2. Config** — Put a YAML/JSON config in `~/.config/openab/` (see [config.example.yaml](config.example.yaml)). You need at least a bot token and an allowlist of user IDs. Full options: [docs/guide.md](docs/guide.md).
50
+
51
+ **3. Run**
52
+
53
+ ```bash
54
+ openab # e.g. Telegram
55
+ openab run-discord # e.g. Discord
56
+ ```
57
+
58
+ Then open the bot in your chat app and send a message.
59
+
60
+ ---
61
+
62
+ ## Docs
63
+
64
+ - **[Configuration & usage](docs/guide.md)** — Config keys, agent setup, commands, security, i18n.
65
+
66
+ ---
67
+
68
+ ## License
69
+
70
+ MIT — [LICENSE](LICENSE)
openab-0.3.0/README.md ADDED
@@ -0,0 +1,53 @@
1
+ <p align="center">
2
+ <img src="assets/icon.png" width="256" alt="OpenAB" />
3
+ </p>
4
+
5
+ # OpenAB
6
+
7
+ **Open Agent Bridge** — Connect AI agents (Cursor, Codex, Gemini, Claude, OpenClaw) to chat platforms. One config, one bridge.
8
+
9
+ [中文](docs/README.zh-CN.md) · [Configuration & usage](docs/guide.md)
10
+
11
+ ---
12
+
13
+ ## What it does
14
+
15
+ OpenAB forwards messages from **chat platforms** to an agent backend you choose and sends the reply back. No per-agent bots — configure once and chat.
16
+
17
+ | Agents | Chats |
18
+ |--------|-------|
19
+ | Cursor, Codex, Gemini, Claude, OpenClaw | Telegram, Discord, _more planned_ |
20
+
21
+ ---
22
+
23
+ ## Quick start
24
+
25
+ **1. Install** (Python 3.10+)
26
+
27
+ ```bash
28
+ pip install openab
29
+ # or: uv tool install openab
30
+ ```
31
+
32
+ **2. Config** — Put a YAML/JSON config in `~/.config/openab/` (see [config.example.yaml](config.example.yaml)). You need at least a bot token and an allowlist of user IDs. Full options: [docs/guide.md](docs/guide.md).
33
+
34
+ **3. Run**
35
+
36
+ ```bash
37
+ openab # e.g. Telegram
38
+ openab run-discord # e.g. Discord
39
+ ```
40
+
41
+ Then open the bot in your chat app and send a message.
42
+
43
+ ---
44
+
45
+ ## Docs
46
+
47
+ - **[Configuration & usage](docs/guide.md)** — Config keys, agent setup, commands, security, i18n.
48
+
49
+ ---
50
+
51
+ ## License
52
+
53
+ MIT — [LICENSE](LICENSE)
@@ -0,0 +1,2 @@
1
+ # OpenAB — Open Agent Bridge
2
+ __version__ = "0.2.0"
@@ -0,0 +1,5 @@
1
+ """Entry point: python -m openab"""
2
+ from openab.cli.main import app
3
+
4
+ if __name__ == "__main__":
5
+ app()
@@ -0,0 +1,71 @@
1
+ """Agent backends: Cursor, Codex, Gemini, Claude, OpenClaw. 由 agent_config 或环境变量指定后端与选项。"""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ from openab.core.i18n import t
10
+
11
+ from . import claude, codex, cursor, gemini, openclaw
12
+
13
+
14
+ def get_backend(agent_config: dict[str, Any] | None = None) -> str:
15
+ if agent_config:
16
+ agent = agent_config.get("agent") or {}
17
+ b = agent.get("backend") or "cursor"
18
+ return str(b).strip().lower()
19
+ return (os.environ.get("OPENAB_AGENT") or "cursor").strip().lower()
20
+
21
+
22
+ def run_agent(
23
+ prompt: str,
24
+ *,
25
+ workspace: Optional[Path] = None,
26
+ timeout: Optional[int] = None,
27
+ agent_config: Optional[dict[str, Any]] = None,
28
+ ) -> str:
29
+ """同步执行当前配置的 agent,返回完整 stdout 文本。"""
30
+ return asyncio.get_event_loop().run_until_complete(
31
+ run_agent_async(
32
+ prompt,
33
+ workspace=workspace,
34
+ timeout=timeout,
35
+ agent_config=agent_config,
36
+ )
37
+ )
38
+
39
+
40
+ async def run_agent_async(
41
+ prompt: str,
42
+ *,
43
+ workspace: Optional[Path] = None,
44
+ timeout: int = 300,
45
+ lang: str = "en",
46
+ agent_config: Optional[dict[str, Any]] = None,
47
+ ) -> str:
48
+ """异步执行 agent;backend 与各后端选项来自 agent_config,缺省时回退到环境变量。"""
49
+ backend = get_backend(agent_config)
50
+ if backend == "codex":
51
+ return await codex.run_async(
52
+ prompt, workspace=workspace, timeout=timeout, lang=lang, agent_config=agent_config
53
+ )
54
+ if backend == "gemini":
55
+ return await gemini.run_async(
56
+ prompt, workspace=workspace, timeout=timeout, lang=lang, agent_config=agent_config
57
+ )
58
+ if backend == "claude":
59
+ return await claude.run_async(
60
+ prompt, workspace=workspace, timeout=timeout, lang=lang, agent_config=agent_config
61
+ )
62
+ if backend == "openclaw":
63
+ return await openclaw.run_async(
64
+ prompt, workspace=workspace, timeout=timeout, lang=lang, agent_config=agent_config
65
+ )
66
+ return await cursor.run_async(
67
+ prompt, workspace=workspace, timeout=timeout, lang=lang, agent_config=agent_config
68
+ )
69
+
70
+
71
+ __all__ = ["run_agent", "run_agent_async", "get_backend"]
@@ -0,0 +1,100 @@
1
+ """Claude Code CLI backend (https://code.claude.com/docs/en/cli-reference).
2
+
3
+ Print mode: claude -p "query" — response to stdout, then exit.
4
+ Flags used: --output-format text, --no-session-persistence; optional: --model, --max-turns, --add-dir.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import os
10
+ import shutil
11
+ from pathlib import Path
12
+ from typing import Any, Optional
13
+
14
+ from openab.core.i18n import t
15
+
16
+
17
+ def _find_cmd(agent_config: dict[str, Any] | None = None) -> str:
18
+ cmd = "claude"
19
+ if agent_config:
20
+ c = (agent_config.get("claude") or {}).get("cmd")
21
+ if c:
22
+ cmd = str(c)
23
+ if not cmd:
24
+ cmd = os.environ.get("CLAUDE_CLI_CMD", "claude")
25
+ if os.path.isabs(cmd):
26
+ return cmd
27
+ exe = shutil.which(cmd)
28
+ return exe or cmd
29
+
30
+
31
+ def _build_args(
32
+ prompt: str,
33
+ workspace: Optional[Path],
34
+ agent_config: dict[str, Any] | None = None,
35
+ ) -> list[str]:
36
+ cmd = _find_cmd(agent_config)
37
+ args = [
38
+ cmd,
39
+ "--print",
40
+ "--output-format", "text",
41
+ "--no-session-persistence",
42
+ ]
43
+ model = ""
44
+ max_turns = ""
45
+ add_dirs: list[str] = []
46
+ if agent_config:
47
+ c = agent_config.get("claude") or {}
48
+ if c.get("model"):
49
+ model = str(c["model"]).strip()
50
+ if c.get("max_turns") is not None:
51
+ max_turns = str(c["max_turns"]).strip()
52
+ ad = c.get("add_dir")
53
+ if isinstance(ad, str) and ad:
54
+ add_dirs = [d.strip() for d in ad.split(os.pathsep) if d.strip()]
55
+ elif isinstance(ad, list):
56
+ add_dirs = [str(x).strip() for x in ad if str(x).strip()]
57
+ if not model:
58
+ model = os.environ.get("CLAUDE_CLI_MODEL", "").strip()
59
+ if not max_turns:
60
+ max_turns = os.environ.get("CLAUDE_CLI_MAX_TURNS", "").strip()
61
+ if not add_dirs:
62
+ raw = os.environ.get("CLAUDE_CLI_ADD_DIR", "").strip()
63
+ if raw:
64
+ add_dirs = [d.strip() for d in raw.split(os.pathsep) if d.strip()]
65
+ if model:
66
+ args.extend(["--model", model])
67
+ if max_turns and max_turns.isdigit():
68
+ args.extend(["--max-turns", max_turns])
69
+ for d in add_dirs:
70
+ args.extend(["--add-dir", d])
71
+ args.append(prompt)
72
+ return args
73
+
74
+
75
+ async def run_async(
76
+ prompt: str,
77
+ *,
78
+ workspace: Optional[Path] = None,
79
+ timeout: int = 300,
80
+ lang: str = "en",
81
+ agent_config: Optional[dict[str, Any]] = None,
82
+ ) -> str:
83
+ """Run Claude Code CLI in print mode; return stdout as reply."""
84
+ args = _build_args(prompt, workspace, agent_config)
85
+ cwd = str(workspace) if workspace else None
86
+ proc = await asyncio.create_subprocess_exec(
87
+ *args,
88
+ stdout=asyncio.subprocess.PIPE,
89
+ stderr=asyncio.subprocess.PIPE,
90
+ env=os.environ.copy(),
91
+ cwd=cwd,
92
+ )
93
+ try:
94
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
95
+ except asyncio.TimeoutError:
96
+ proc.kill()
97
+ await proc.wait()
98
+ return t(lang, "agent_timeout")
99
+ text = (stdout or b"").decode("utf-8", errors="replace").strip()
100
+ return text or t(lang, "agent_no_output")
@@ -0,0 +1,64 @@
1
+ """OpenAI Codex CLI backend."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import os
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Any, Optional
9
+
10
+ from openab.core.i18n import t
11
+
12
+
13
+ def _find_cmd(agent_config: dict[str, Any] | None = None) -> str:
14
+ cmd = "codex"
15
+ if agent_config:
16
+ c = (agent_config.get("codex") or {}).get("cmd")
17
+ if c:
18
+ cmd = str(c)
19
+ if not cmd:
20
+ cmd = os.environ.get("CODEX_CMD", "codex")
21
+ if os.path.isabs(cmd):
22
+ return cmd
23
+ exe = shutil.which(cmd)
24
+ return exe or cmd
25
+
26
+
27
+ def _skip_git_check(agent_config: dict[str, Any] | None = None) -> bool:
28
+ if agent_config:
29
+ v = (agent_config.get("codex") or {}).get("skip_git_check")
30
+ if v in (True, "true", "1", 1):
31
+ return True
32
+ return os.environ.get("CODEX_SKIP_GIT_CHECK", "").strip().lower() in ("1", "true", "yes")
33
+
34
+
35
+ async def run_async(
36
+ prompt: str,
37
+ *,
38
+ workspace: Optional[Path] = None,
39
+ timeout: int = 300,
40
+ lang: str = "en",
41
+ agent_config: Optional[dict[str, Any]] = None,
42
+ ) -> str:
43
+ """Codex CLI: codex exec --ephemeral, final message on stdout."""
44
+ cmd = _find_cmd(agent_config)
45
+ args = [cmd, "exec", "--ephemeral"]
46
+ if _skip_git_check(agent_config):
47
+ args.append("--skip-git-repo-check")
48
+ args.append(prompt)
49
+ cwd = str(workspace) if workspace else None
50
+ proc = await asyncio.create_subprocess_exec(
51
+ *args,
52
+ stdout=asyncio.subprocess.PIPE,
53
+ stderr=asyncio.subprocess.DEVNULL,
54
+ env=os.environ.copy(),
55
+ cwd=cwd,
56
+ )
57
+ try:
58
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
59
+ except asyncio.TimeoutError:
60
+ proc.kill()
61
+ await proc.wait()
62
+ return t(lang, "agent_timeout")
63
+ text = (stdout or b"").decode("utf-8", errors="replace").strip()
64
+ return text or t(lang, "agent_no_output")
@@ -0,0 +1,62 @@
1
+ """Cursor Agent CLI backend."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import os
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Any, Optional
9
+
10
+ from openab.core.i18n import t
11
+
12
+
13
+ def _find_cmd(agent_config: dict[str, Any] | None = None) -> str:
14
+ cmd = "agent"
15
+ if agent_config:
16
+ c = (agent_config.get("cursor") or {}).get("cmd")
17
+ if c:
18
+ cmd = str(c)
19
+ if not cmd:
20
+ cmd = os.environ.get("CURSOR_AGENT_CMD", "agent")
21
+ if os.path.isabs(cmd):
22
+ return cmd
23
+ exe = shutil.which(cmd)
24
+ return exe or cmd
25
+
26
+
27
+ async def run_async(
28
+ prompt: str,
29
+ *,
30
+ workspace: Optional[Path] = None,
31
+ timeout: int = 300,
32
+ lang: str = "en",
33
+ agent_config: Optional[dict[str, Any]] = None,
34
+ ) -> str:
35
+ """Cursor Agent CLI: agent --print --trust."""
36
+ cmd = _find_cmd(agent_config)
37
+ base_args = [
38
+ cmd,
39
+ "agent",
40
+ "--print",
41
+ "--output-format", "text",
42
+ "--trust",
43
+ ]
44
+ if workspace is not None:
45
+ base_args.extend(["--workspace", str(workspace)])
46
+ base_args.extend(["--", prompt])
47
+ env = os.environ.copy()
48
+ proc = await asyncio.create_subprocess_exec(
49
+ *base_args,
50
+ stdout=asyncio.subprocess.PIPE,
51
+ stderr=asyncio.subprocess.STDOUT,
52
+ env=env,
53
+ cwd=str(workspace) if workspace else None,
54
+ )
55
+ try:
56
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
57
+ except asyncio.TimeoutError:
58
+ proc.kill()
59
+ await proc.wait()
60
+ return t(lang, "agent_timeout")
61
+ text = (stdout or b"").decode("utf-8", errors="replace").strip()
62
+ return text or t(lang, "agent_no_output")
@@ -0,0 +1,53 @@
1
+ """Gemini CLI backend (google-gemini/gemini-cli)."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import os
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Any, Optional
9
+
10
+ from openab.core.i18n import t
11
+
12
+
13
+ def _find_cmd(agent_config: dict[str, Any] | None = None) -> str:
14
+ cmd = "gemini"
15
+ if agent_config:
16
+ c = (agent_config.get("gemini") or {}).get("cmd")
17
+ if c:
18
+ cmd = str(c)
19
+ if not cmd:
20
+ cmd = os.environ.get("GEMINI_CLI_CMD", "gemini")
21
+ if os.path.isabs(cmd):
22
+ return cmd
23
+ exe = shutil.which(cmd)
24
+ return exe or cmd
25
+
26
+
27
+ async def run_async(
28
+ prompt: str,
29
+ *,
30
+ workspace: Optional[Path] = None,
31
+ timeout: int = 300,
32
+ lang: str = "en",
33
+ agent_config: Optional[dict[str, Any]] = None,
34
+ ) -> str:
35
+ """Gemini CLI: gemini -p \"prompt\" → stdout."""
36
+ cmd = _find_cmd(agent_config)
37
+ args = [cmd, "-p", prompt]
38
+ cwd = str(workspace) if workspace else None
39
+ proc = await asyncio.create_subprocess_exec(
40
+ *args,
41
+ stdout=asyncio.subprocess.PIPE,
42
+ stderr=asyncio.subprocess.PIPE,
43
+ env=os.environ.copy(),
44
+ cwd=cwd,
45
+ )
46
+ try:
47
+ stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=timeout)
48
+ except asyncio.TimeoutError:
49
+ proc.kill()
50
+ await proc.wait()
51
+ return t(lang, "agent_timeout")
52
+ text = (stdout or b"").decode("utf-8", errors="replace").strip()
53
+ return text or t(lang, "agent_no_output")
@@ -0,0 +1,77 @@
1
+ """OpenClaw 后端:通过 CLI `openclaw agent --message "..."` 调用,需先运行 OpenClaw Gateway(或使用本地回退)。
2
+
3
+ 参考:https://docs.openclaw.ai/tools/agent-send
4
+ 安装:npm install -g openclaw@latest,并运行 openclaw onboard / openclaw gateway。
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import os
10
+ import shutil
11
+ from pathlib import Path
12
+ from typing import Any, Optional
13
+
14
+ from openab.core.i18n import t
15
+
16
+
17
+ def _find_cmd(agent_config: dict[str, Any] | None = None) -> str:
18
+ cmd = "openclaw"
19
+ if agent_config:
20
+ c = (agent_config.get("openclaw") or {}).get("cmd")
21
+ if c:
22
+ cmd = str(c)
23
+ if not cmd:
24
+ cmd = os.environ.get("OPENCLAW_CMD", "openclaw")
25
+ if os.path.isabs(cmd):
26
+ return cmd
27
+ exe = shutil.which(cmd)
28
+ return exe or cmd
29
+
30
+
31
+ def _strip_media_lines(text: str) -> str:
32
+ """去掉 OpenClaw 输出中的 MEDIA: 行,只保留回复正文。"""
33
+ lines = []
34
+ for line in text.splitlines():
35
+ if line.strip().startswith("MEDIA:"):
36
+ continue
37
+ lines.append(line)
38
+ return "\n".join(lines).strip()
39
+
40
+
41
+ async def run_async(
42
+ prompt: str,
43
+ *,
44
+ workspace: Optional[Path] = None,
45
+ timeout: int = 300,
46
+ lang: str = "en",
47
+ agent_config: Optional[dict[str, Any]] = None,
48
+ ) -> str:
49
+ """调用 openclaw agent --message \"<prompt>\",从 stdout 取回复;默认会过滤 MEDIA: 行。"""
50
+ cmd = _find_cmd(agent_config)
51
+ args = [cmd, "agent", "--message", prompt]
52
+ oc_cfg = (agent_config or {}).get("openclaw") or {}
53
+ to = oc_cfg.get("timeout") or timeout
54
+ if to and int(to) > 0:
55
+ args.extend(["--timeout", str(int(to))])
56
+ thinking = (oc_cfg.get("thinking") or "").strip().lower()
57
+ if thinking in ("off", "minimal", "low", "medium", "high", "xhigh"):
58
+ args.extend(["--thinking", thinking])
59
+ cwd = str(workspace) if workspace else None
60
+ proc = await asyncio.create_subprocess_exec(
61
+ *args,
62
+ stdout=asyncio.subprocess.PIPE,
63
+ stderr=asyncio.subprocess.PIPE,
64
+ env=os.environ.copy(),
65
+ cwd=cwd,
66
+ )
67
+ try:
68
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
69
+ except asyncio.TimeoutError:
70
+ proc.kill()
71
+ await proc.wait()
72
+ return t(lang, "agent_timeout")
73
+ text = (stdout or b"").decode("utf-8", errors="replace")
74
+ text = _strip_media_lines(text)
75
+ if not text.strip():
76
+ return t(lang, "agent_no_output")
77
+ return text
@@ -0,0 +1 @@
1
+ # Chat frontends: Telegram, (future: Discord, Slack, …)
@@ -0,0 +1,3 @@
1
+ from .bot import run_bot
2
+
3
+ __all__ = ["run_bot"]