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 +21 -0
- openab-0.3.0/PKG-INFO +70 -0
- openab-0.3.0/README.md +53 -0
- openab-0.3.0/openab/__init__.py +2 -0
- openab-0.3.0/openab/__main__.py +5 -0
- openab-0.3.0/openab/agents/__init__.py +71 -0
- openab-0.3.0/openab/agents/claude.py +100 -0
- openab-0.3.0/openab/agents/codex.py +64 -0
- openab-0.3.0/openab/agents/cursor.py +62 -0
- openab-0.3.0/openab/agents/gemini.py +53 -0
- openab-0.3.0/openab/agents/openclaw.py +77 -0
- openab-0.3.0/openab/chats/__init__.py +1 -0
- openab-0.3.0/openab/chats/discord/__init__.py +3 -0
- openab-0.3.0/openab/chats/discord/bot.py +167 -0
- openab-0.3.0/openab/chats/telegram/__init__.py +3 -0
- openab-0.3.0/openab/chats/telegram/bot.py +177 -0
- openab-0.3.0/openab/cli/__init__.py +3 -0
- openab-0.3.0/openab/cli/main.py +168 -0
- openab-0.3.0/openab/core/__init__.py +3 -0
- openab-0.3.0/openab/core/config.py +152 -0
- openab-0.3.0/openab/core/i18n.py +138 -0
- openab-0.3.0/openab.egg-info/PKG-INFO +70 -0
- openab-0.3.0/openab.egg-info/SOURCES.txt +27 -0
- openab-0.3.0/openab.egg-info/dependency_links.txt +1 -0
- openab-0.3.0/openab.egg-info/entry_points.txt +2 -0
- openab-0.3.0/openab.egg-info/requires.txt +8 -0
- openab-0.3.0/openab.egg-info/top_level.txt +1 -0
- openab-0.3.0/pyproject.toml +27 -0
- openab-0.3.0/setup.cfg +4 -0
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,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, …)
|