Skip to content

Commit 3f00fae

Browse files
Merge pull request #1 from Grigory-Rylov/opencode-appname-fixes
feat: opencode app-name support, build setup, and per-app dir fix
2 parents 9a1e2ea + d7e3ffa commit 3f00fae

30 files changed

Lines changed: 5277 additions & 662 deletions

File tree

.mimocode/plans/1781675933910-neon-mountain.md

Lines changed: 0 additions & 48 deletions
This file was deleted.

bot/config.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
"thinking_peer_id": 2000000506,
1616
"llama_server_path": "llama-server",
1717
"llama_server_host": "http://localhost:8081", # URL удалённого llama-server
18-
"opencode_config_path": "~/.config/lildax/config.json",
18+
"opencode_config_path": "~/.config/opencode/config.json",
1919
"models": [],
2020
"default_model": "qwen3.5-122b",
21+
"allowed_folders": ["/tmp"],
2122
}
2223

2324

@@ -46,7 +47,7 @@ def load_config(config_path: str = "config.json") -> dict:
4647
)
4748
parser.add_argument(
4849
"--opencode-bin", type=str, default=None,
49-
help="Path to opencode binary (lildax). Overrides opencode_bin_path from config."
50+
help="Path to opencode binary. Overrides opencode_bin_path from config."
5051
)
5152
parser.add_argument(
5253
"--llama-host", type=str, default=None,
@@ -64,7 +65,7 @@ def load_config(config_path: str = "config.json") -> dict:
6465

6566
# ---------- Глобальные константы из конфигурации ----------
6667
VK_TOKEN = CONFIG["vk_token"]
67-
OPENCODE_URL = "http://127.0.0.1:4098" #CONFIG["opencode_url"]
68+
OPENCODE_URL = CONFIG["opencode_url"]
6869
SESSION_FILE = Path(CONFIG["session_file"])
6970
VK_API_VERSION = CONFIG["vk_api_version"]
7071
LONGPOLL_WAIT = CONFIG["longpoll_wait"]
@@ -76,6 +77,8 @@ def load_config(config_path: str = "config.json") -> dict:
7677
LLAMA_SERVER_HOST = CONFIG.get("llama_server_host", "http://localhost:8081")
7778
MCP_SERVERS = CONFIG.get("mcp_servers", {})
7879
SUBAGENT_PREFIX = CONFIG.get("subagent_prefix", "[subagent] ")
80+
OPENCODE_APP_NAME = CONFIG.get("opencode-app-name")
81+
ALLOWED_FOLDERS = CONFIG.get("allowed_folders", ["/tmp"])
7982

8083
if not VK_TOKEN:
8184
raise ValueError("VK_TOKEN is required in config file")
@@ -151,6 +154,8 @@ def switch_config(config_name: str) -> bool:
151154
current_module.LLAMA_SERVER_HOST = new_config.get("llama_server_host", "http://localhost:8081")
152155
current_module.MCP_SERVERS = new_config.get("mcp_servers", {})
153156
current_module.SUBAGENT_PREFIX = new_config.get("subagent_prefix", "[subagent] ")
157+
current_module.OPENCODE_APP_NAME = new_config.get("opencode-app-name")
158+
current_module.ALLOWED_FOLDERS = new_config.get("allowed_folders", ["/tmp"])
154159
current_module.OPENCODE_BIN = Path(new_config["opencode_bin_path"])
155160
if not current_module.OPENCODE_BIN.is_absolute():
156161
current_module.OPENCODE_BIN = (SCRIPT_DIR / current_module.OPENCODE_BIN).resolve()

bot/opencode_client.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
"""
22
HTTP клиент для OpenCode API
3+
4+
Использует opencode API (/session/...) вместо v2 API (/api/session/...).
5+
v2 API был для lildax и использует SessionExecution.noopLayer - agent loop не работает.
6+
opencode API использует SessionPrompt с реальным agent loop.
37
"""
48
import json
59
from typing import Dict, List, Optional
@@ -42,11 +46,11 @@ def session(self) -> ClientSession:
4246
async def create_session(self, model: str = None) -> str:
4347
"""Создаёт новую сессию и возвращает её ID."""
4448
data = model_to_api_format(model or config.CLI_MODEL)
45-
async with self.session.post(f"{self.base_url}/api/session", json=data) as resp:
49+
# opencode API: POST /session
50+
async with self.session.post(f"{self.base_url}/session", json=data) as resp:
4651
resp.raise_for_status()
4752
resp_data = await resp.json()
48-
# API возвращает {data: {id: ...}}
49-
session_id = resp_data.get("data", resp_data).get("id")
53+
session_id = resp_data.get("id", resp_data.get("data", {}).get("id"))
5054
logger.debug(f"Created OpenCode session {session_id}")
5155
return session_id
5256

@@ -55,41 +59,47 @@ async def get_session_messages(
5559
) -> Optional[List[dict]]:
5660
"""Получает сообщения сессии."""
5761
try:
58-
url = f"{self.base_url}/api/session/{session_id}/message?limit={limit}"
62+
# opencode API: GET /session/:sessionID/message
63+
url = f"{self.base_url}/session/{session_id}/message?limit={limit}"
5964
async with self.session.get(url) as resp:
6065
if resp.status != 200:
6166
return None
6267
resp_data = await resp.json()
63-
# API возвращает {data: [...], cursor: {...}}
64-
return resp_data.get("data", resp_data)
68+
return resp_data if isinstance(resp_data, list) else resp_data.get("data", resp_data)
6569
except Exception as e:
6670
logger.warning(f"Failed to fetch messages for {session_id}: {e}")
6771
return None
6872

6973
async def send_prompt(
7074
self, session_id: str, text: str
7175
) -> bool:
72-
"""Отправляет промпт в сессию."""
73-
url = f"{self.base_url}/api/session/{session_id}/prompt"
74-
# API требует формат {prompt: {text: "..."}}
75-
data = {"prompt": {"text": text}}
76+
"""Отправляет промпт в сессию.
77+
78+
Использует /session/:sessionID/prompt_async endpoint opencode API,
79+
который реально запускает agent loop.
80+
"""
81+
# opencode API: POST /session/:sessionID/prompt_async
82+
# Формат: {parts: [{type: "text", text: "..."}]}
83+
url = f"{self.base_url}/session/{session_id}/prompt_async"
84+
data = {"parts": [{"type": "text", "text": text}]}
7685
async with self.session.post(url, json=data) as resp:
77-
if resp.status == 200:
86+
if resp.status in (200, 204):
7887
logger.debug(f"Prompt sent for session {session_id}")
7988
return True
80-
logger.error(f"Failed to send prompt: {resp.status}")
89+
text = await resp.text()
90+
logger.error(f"Failed to send prompt: {resp.status} - {text}")
8191
return False
8292

8393
# ---------- Разрешения ----------
8494

8595
async def get_pending_permissions(self) -> List[dict]:
8696
"""Получает список ожидающих разрешений."""
8797
try:
88-
async with self.session.get(f"{self.base_url}/api/permission/request") as resp:
98+
# opencode API: GET /permission/request (если есть)
99+
async with self.session.get(f"{self.base_url}/permission/request") as resp:
89100
if resp.status != 200:
90101
return []
91102
resp_data = await resp.json()
92-
# API возвращает {location: {...}, data: [...]}
93103
return resp_data.get("data", resp_data)
94104
except Exception as e:
95105
logger.warning(f"Error fetching permissions: {e}")
@@ -104,8 +114,9 @@ async def send_permission_response(
104114
"""
105115
reply_map = {"always": "always", "once": "once", "never": "reject"}
106116
reply = reply_map.get(response, response)
107-
url = f"{self.base_url}/api/session/{session_id}/permission/{permission_id}/reply"
108-
data = {"reply": reply}
117+
# opencode API: POST /session/:sessionID/permissions/:permissionID
118+
url = f"{self.base_url}/session/{session_id}/permissions/{permission_id}"
119+
data = {"response": reply}
109120
async with self.session.post(url, json=data) as resp:
110121
if resp.status in (200, 204):
111122
logger.debug(f"Permission {permission_id} answered: {reply}")
@@ -118,11 +129,10 @@ async def send_permission_response(
118129
async def get_pending_questions(self) -> List[dict]:
119130
"""Получает список ожидающих вопросов."""
120131
try:
121-
async with self.session.get(f"{self.base_url}/api/question/request") as resp:
132+
async with self.session.get(f"{self.base_url}/question/request") as resp:
122133
if resp.status != 200:
123134
return []
124135
resp_data = await resp.json()
125-
# API возвращает {location: {...}, data: [...]}
126136
return resp_data.get("data", resp_data)
127137
except Exception as e:
128138
logger.warning(f"Error fetching questions: {e}")
@@ -132,7 +142,7 @@ async def send_question_answer(
132142
self, session_id: str, question_id: str, answer: str
133143
) -> bool:
134144
"""Отправляет ответ на вопрос."""
135-
url = f"{self.base_url}/api/session/{session_id}/question/{question_id}/reply"
145+
url = f"{self.base_url}/session/{session_id}/question/{question_id}/reply"
136146
data = {"answers": [[answer]]}
137147
async with self.session.post(url, json=data) as resp:
138148
if resp.status in (200, 204):
@@ -146,12 +156,12 @@ async def send_question_answer(
146156
async def get_all_sessions(self) -> List[dict]:
147157
"""Получает список всех сессий."""
148158
try:
149-
async with self.session.get(f"{self.base_url}/api/session") as resp:
159+
# opencode API: GET /session
160+
async with self.session.get(f"{self.base_url}/session") as resp:
150161
if resp.status != 200:
151162
return []
152163
resp_data = await resp.json()
153-
# API возвращает {data: [...], cursor: {...}}
154-
return resp_data.get("data", resp_data)
164+
return resp_data if isinstance(resp_data, list) else resp_data.get("data", resp_data)
155165
except Exception as e:
156166
logger.warning(f"Error fetching sessions list: {e}")
157167
return []
@@ -164,7 +174,8 @@ async def get_child_sessions(self, parent_id: str) -> List[dict]:
164174
async def delete_session(self, session_id: str) -> bool:
165175
"""Удаляет сессию по ID."""
166176
try:
167-
async with self.session.delete(f"{self.base_url}/api/session/{session_id}") as resp:
177+
# opencode API: DELETE /session/:sessionID
178+
async with self.session.delete(f"{self.base_url}/session/{session_id}") as resp:
168179
if resp.status in (200, 204):
169180
logger.debug(f"Deleted session {session_id}")
170181
return True

0 commit comments

Comments
 (0)