vibe-remote 2.1.6__py3-none-any.whl
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.
- config/__init__.py +37 -0
- config/paths.py +56 -0
- config/v2_compat.py +74 -0
- config/v2_config.py +206 -0
- config/v2_sessions.py +73 -0
- config/v2_settings.py +115 -0
- core/__init__.py +0 -0
- core/controller.py +736 -0
- core/handlers/__init__.py +13 -0
- core/handlers/command_handlers.py +342 -0
- core/handlers/message_handler.py +365 -0
- core/handlers/session_handler.py +233 -0
- core/handlers/settings_handler.py +362 -0
- modules/__init__.py +0 -0
- modules/agent_router.py +58 -0
- modules/agents/__init__.py +38 -0
- modules/agents/base.py +91 -0
- modules/agents/claude_agent.py +344 -0
- modules/agents/codex_agent.py +368 -0
- modules/agents/opencode_agent.py +2155 -0
- modules/agents/service.py +41 -0
- modules/agents/subagent_router.py +136 -0
- modules/claude_client.py +154 -0
- modules/im/__init__.py +63 -0
- modules/im/base.py +323 -0
- modules/im/factory.py +60 -0
- modules/im/formatters/__init__.py +4 -0
- modules/im/formatters/base_formatter.py +639 -0
- modules/im/formatters/slack_formatter.py +127 -0
- modules/im/slack.py +2091 -0
- modules/session_manager.py +138 -0
- modules/settings_manager.py +587 -0
- vibe/__init__.py +6 -0
- vibe/__main__.py +12 -0
- vibe/_version.py +34 -0
- vibe/api.py +412 -0
- vibe/cli.py +637 -0
- vibe/runtime.py +213 -0
- vibe/service_main.py +101 -0
- vibe/templates/slack_manifest.json +65 -0
- vibe/ui/dist/assets/index-8g3mNwMK.js +35 -0
- vibe/ui/dist/assets/index-M55aMB5R.css +1 -0
- vibe/ui/dist/assets/logo-BzryTZ7u.png +0 -0
- vibe/ui/dist/index.html +17 -0
- vibe/ui/dist/logo.png +0 -0
- vibe/ui/dist/vite.svg +1 -0
- vibe/ui_server.py +346 -0
- vibe_remote-2.1.6.dist-info/METADATA +295 -0
- vibe_remote-2.1.6.dist-info/RECORD +52 -0
- vibe_remote-2.1.6.dist-info/WHEEL +4 -0
- vibe_remote-2.1.6.dist-info/entry_points.txt +2 -0
- vibe_remote-2.1.6.dist-info/licenses/LICENSE +21 -0
vibe/api.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from config import paths
|
|
12
|
+
from config.v2_config import V2Config
|
|
13
|
+
from config.v2_settings import (
|
|
14
|
+
SettingsStore,
|
|
15
|
+
ChannelSettings,
|
|
16
|
+
RoutingSettings,
|
|
17
|
+
normalize_show_message_types,
|
|
18
|
+
)
|
|
19
|
+
from config.v2_sessions import SessionsStore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_OPENCODE_OPTIONS_CACHE: dict[str, Optional[object]] = {
|
|
25
|
+
"data": None,
|
|
26
|
+
"updated_at": 0.0,
|
|
27
|
+
}
|
|
28
|
+
_OPENCODE_OPTIONS_TTL_SECONDS = 30.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_config() -> V2Config:
|
|
32
|
+
return V2Config.load()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def save_config(payload: dict) -> V2Config:
|
|
36
|
+
config = V2Config.from_payload(payload)
|
|
37
|
+
config.save()
|
|
38
|
+
return config
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def config_to_payload(config: V2Config) -> dict:
|
|
42
|
+
payload = {
|
|
43
|
+
"mode": config.mode,
|
|
44
|
+
"version": config.version,
|
|
45
|
+
"slack": {
|
|
46
|
+
**config.slack.__dict__,
|
|
47
|
+
"require_mention": config.slack.require_mention,
|
|
48
|
+
},
|
|
49
|
+
"runtime": {
|
|
50
|
+
"default_cwd": config.runtime.default_cwd,
|
|
51
|
+
"log_level": config.runtime.log_level,
|
|
52
|
+
},
|
|
53
|
+
"slack": {
|
|
54
|
+
**config.slack.__dict__,
|
|
55
|
+
"require_mention": config.slack.require_mention,
|
|
56
|
+
},
|
|
57
|
+
"agents": {
|
|
58
|
+
"default_backend": config.agents.default_backend,
|
|
59
|
+
"opencode": config.agents.opencode.__dict__,
|
|
60
|
+
"claude": config.agents.claude.__dict__,
|
|
61
|
+
"codex": config.agents.codex.__dict__,
|
|
62
|
+
},
|
|
63
|
+
"gateway": config.gateway.__dict__ if config.gateway else None,
|
|
64
|
+
"ui": config.ui.__dict__,
|
|
65
|
+
"ack_mode": config.ack_mode,
|
|
66
|
+
}
|
|
67
|
+
return payload
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_settings() -> dict:
|
|
71
|
+
store = SettingsStore()
|
|
72
|
+
return _settings_to_payload(store)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def save_settings(payload: dict) -> dict:
|
|
76
|
+
store = SettingsStore()
|
|
77
|
+
channels = {}
|
|
78
|
+
for channel_id, channel_payload in (payload.get("channels") or {}).items():
|
|
79
|
+
routing_payload = channel_payload.get("routing") or {}
|
|
80
|
+
routing = RoutingSettings(
|
|
81
|
+
agent_backend=routing_payload.get("agent_backend"),
|
|
82
|
+
opencode_agent=routing_payload.get("opencode_agent"),
|
|
83
|
+
opencode_model=routing_payload.get("opencode_model"),
|
|
84
|
+
opencode_reasoning_effort=routing_payload.get("opencode_reasoning_effort"),
|
|
85
|
+
)
|
|
86
|
+
channels[channel_id] = ChannelSettings(
|
|
87
|
+
enabled=channel_payload.get("enabled", True),
|
|
88
|
+
show_message_types=normalize_show_message_types(
|
|
89
|
+
channel_payload.get("show_message_types")
|
|
90
|
+
),
|
|
91
|
+
custom_cwd=channel_payload.get("custom_cwd"),
|
|
92
|
+
routing=routing,
|
|
93
|
+
require_mention=channel_payload.get("require_mention"),
|
|
94
|
+
)
|
|
95
|
+
store.settings.channels = channels
|
|
96
|
+
store.save()
|
|
97
|
+
return _settings_to_payload(store)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def init_sessions() -> None:
|
|
101
|
+
store = SessionsStore()
|
|
102
|
+
store.save()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def detect_cli(binary: str) -> dict:
|
|
106
|
+
if binary == "claude":
|
|
107
|
+
preferred = Path.home() / ".claude" / "local" / "claude"
|
|
108
|
+
if preferred.exists() and os.access(preferred, os.X_OK):
|
|
109
|
+
return {"found": True, "path": str(preferred)}
|
|
110
|
+
path = shutil.which(binary)
|
|
111
|
+
if not path:
|
|
112
|
+
return {"found": False, "path": None}
|
|
113
|
+
return {"found": True, "path": path}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def check_cli_exec(path: str) -> dict:
|
|
117
|
+
if not path:
|
|
118
|
+
return {"ok": False, "error": "path is empty"}
|
|
119
|
+
if not os.path.exists(path):
|
|
120
|
+
return {"ok": False, "error": "path does not exist"}
|
|
121
|
+
if not os.access(path, os.X_OK):
|
|
122
|
+
return {"ok": False, "error": "path is not executable"}
|
|
123
|
+
return {"ok": True}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def slack_auth_test(bot_token: str) -> dict:
|
|
127
|
+
try:
|
|
128
|
+
from slack_sdk.web import WebClient
|
|
129
|
+
|
|
130
|
+
client = WebClient(token=bot_token)
|
|
131
|
+
response = client.auth_test()
|
|
132
|
+
return {"ok": True, "response": response.data}
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
return {"ok": False, "error": str(exc)}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def list_channels(bot_token: str) -> dict:
|
|
138
|
+
try:
|
|
139
|
+
from slack_sdk.web import WebClient
|
|
140
|
+
|
|
141
|
+
client = WebClient(token=bot_token)
|
|
142
|
+
channels = []
|
|
143
|
+
cursor = None
|
|
144
|
+
while True:
|
|
145
|
+
response = client.conversations_list(
|
|
146
|
+
types="public_channel,private_channel",
|
|
147
|
+
limit=200,
|
|
148
|
+
cursor=cursor,
|
|
149
|
+
)
|
|
150
|
+
for channel in response.get("channels", []):
|
|
151
|
+
channels.append({
|
|
152
|
+
"id": channel.get("id"),
|
|
153
|
+
"name": channel.get("name"),
|
|
154
|
+
"is_private": channel.get("is_private", False),
|
|
155
|
+
})
|
|
156
|
+
cursor = response.get("response_metadata", {}).get("next_cursor")
|
|
157
|
+
if not cursor:
|
|
158
|
+
break
|
|
159
|
+
return {"ok": True, "channels": channels}
|
|
160
|
+
except Exception as exc:
|
|
161
|
+
return {"ok": False, "error": str(exc)}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def opencode_options(cwd: str) -> dict:
|
|
165
|
+
try:
|
|
166
|
+
return asyncio.run(opencode_options_async(cwd))
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
logger.warning("OpenCode options fetch failed: %s", exc, exc_info=True)
|
|
169
|
+
return {"ok": False, "error": str(exc)}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def opencode_options_async(cwd: str) -> dict:
|
|
173
|
+
cache_data = _OPENCODE_OPTIONS_CACHE.get("data")
|
|
174
|
+
updated_at = _OPENCODE_OPTIONS_CACHE.get("updated_at")
|
|
175
|
+
updated_at_value = updated_at if isinstance(updated_at, float) else 0.0
|
|
176
|
+
cache_age = time.monotonic() - updated_at_value
|
|
177
|
+
if cache_data and cache_age < _OPENCODE_OPTIONS_TTL_SECONDS:
|
|
178
|
+
return {"ok": True, "data": cache_data, "cached": True}
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
from config.v2_compat import to_app_config
|
|
182
|
+
from modules.agents.opencode_agent import (
|
|
183
|
+
OpenCodeServerManager,
|
|
184
|
+
build_reasoning_effort_options,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
config = to_app_config(V2Config.load())
|
|
188
|
+
if not config.opencode:
|
|
189
|
+
return {"ok": False, "error": "opencode disabled"}
|
|
190
|
+
opencode_config = config.opencode
|
|
191
|
+
timeout_seconds = min(10.0, float(opencode_config.request_timeout_seconds or 10))
|
|
192
|
+
|
|
193
|
+
def _build_reasoning_options(
|
|
194
|
+
models: dict,
|
|
195
|
+
builder,
|
|
196
|
+
) -> dict:
|
|
197
|
+
options: dict = {}
|
|
198
|
+
for provider in models.get("providers", []):
|
|
199
|
+
provider_id = (
|
|
200
|
+
provider.get("id")
|
|
201
|
+
or provider.get("provider_id")
|
|
202
|
+
or provider.get("name")
|
|
203
|
+
)
|
|
204
|
+
if not provider_id:
|
|
205
|
+
continue
|
|
206
|
+
model_ids = []
|
|
207
|
+
provider_models = provider.get("models", {})
|
|
208
|
+
if isinstance(provider_models, dict):
|
|
209
|
+
model_ids = list(provider_models.keys())
|
|
210
|
+
elif isinstance(provider_models, list):
|
|
211
|
+
model_ids = [
|
|
212
|
+
model.get("id")
|
|
213
|
+
for model in provider_models
|
|
214
|
+
if isinstance(model, dict) and model.get("id")
|
|
215
|
+
]
|
|
216
|
+
for model_id in model_ids:
|
|
217
|
+
model_key = f"{provider_id}/{model_id}"
|
|
218
|
+
options[model_key] = builder(models, model_key)
|
|
219
|
+
return options
|
|
220
|
+
|
|
221
|
+
server = await OpenCodeServerManager.get_instance(
|
|
222
|
+
binary=opencode_config.binary,
|
|
223
|
+
port=opencode_config.port,
|
|
224
|
+
request_timeout_seconds=opencode_config.request_timeout_seconds,
|
|
225
|
+
)
|
|
226
|
+
await asyncio.wait_for(server.ensure_running(), timeout=timeout_seconds)
|
|
227
|
+
agents = await asyncio.wait_for(server.get_available_agents(cwd), timeout=timeout_seconds)
|
|
228
|
+
models = await asyncio.wait_for(server.get_available_models(cwd), timeout=timeout_seconds)
|
|
229
|
+
defaults = await asyncio.wait_for(server.get_default_config(cwd), timeout=timeout_seconds)
|
|
230
|
+
reasoning_options = _build_reasoning_options(models, build_reasoning_effort_options)
|
|
231
|
+
data = {
|
|
232
|
+
"agents": agents,
|
|
233
|
+
"models": models,
|
|
234
|
+
"defaults": defaults,
|
|
235
|
+
"reasoning_options": reasoning_options,
|
|
236
|
+
}
|
|
237
|
+
_OPENCODE_OPTIONS_CACHE["data"] = data
|
|
238
|
+
_OPENCODE_OPTIONS_CACHE["updated_at"] = time.monotonic()
|
|
239
|
+
return {"ok": True, "data": data}
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
logger.warning("OpenCode options fetch failed: %s", exc, exc_info=True)
|
|
242
|
+
if cache_data:
|
|
243
|
+
return {"ok": True, "data": cache_data, "cached": True, "warning": str(exc)}
|
|
244
|
+
return {"ok": False, "error": str(exc)}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _settings_to_payload(store: SettingsStore) -> dict:
|
|
248
|
+
payload = {"channels": {}}
|
|
249
|
+
for channel_id, settings in store.settings.channels.items():
|
|
250
|
+
payload["channels"][channel_id] = {
|
|
251
|
+
"enabled": settings.enabled,
|
|
252
|
+
"show_message_types": normalize_show_message_types(
|
|
253
|
+
settings.show_message_types
|
|
254
|
+
),
|
|
255
|
+
"custom_cwd": settings.custom_cwd,
|
|
256
|
+
"require_mention": settings.require_mention,
|
|
257
|
+
"routing": {
|
|
258
|
+
"agent_backend": settings.routing.agent_backend,
|
|
259
|
+
"opencode_agent": settings.routing.opencode_agent,
|
|
260
|
+
"opencode_model": settings.routing.opencode_model,
|
|
261
|
+
"opencode_reasoning_effort": settings.routing.opencode_reasoning_effort,
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
return payload
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def get_slack_manifest() -> dict:
|
|
268
|
+
"""Get Slack App Manifest template for self-host mode.
|
|
269
|
+
|
|
270
|
+
Loads manifest from vibe/templates/slack_manifest.json.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
{"ok": True, "manifest": str, "manifest_compact": str} on success
|
|
274
|
+
{"ok": False, "error": str} on failure
|
|
275
|
+
"""
|
|
276
|
+
import json
|
|
277
|
+
import importlib.resources
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
manifest = None
|
|
281
|
+
|
|
282
|
+
# Try to load from package resources (installed via pip/uv)
|
|
283
|
+
try:
|
|
284
|
+
if hasattr(importlib.resources, 'files'):
|
|
285
|
+
package_files = importlib.resources.files('vibe')
|
|
286
|
+
template_path = package_files / 'templates' / 'slack_manifest.json'
|
|
287
|
+
if hasattr(template_path, 'read_text'):
|
|
288
|
+
manifest = json.loads(template_path.read_text(encoding='utf-8'))
|
|
289
|
+
except (TypeError, FileNotFoundError, AttributeError, json.JSONDecodeError):
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
# Fallback: load from file system (development mode)
|
|
293
|
+
if manifest is None:
|
|
294
|
+
this_dir = Path(__file__).parent
|
|
295
|
+
template_file = this_dir / 'templates' / 'slack_manifest.json'
|
|
296
|
+
if template_file.exists():
|
|
297
|
+
manifest = json.loads(template_file.read_text(encoding='utf-8'))
|
|
298
|
+
|
|
299
|
+
if manifest is None:
|
|
300
|
+
return {"ok": False, "error": "Manifest template file not found"}
|
|
301
|
+
|
|
302
|
+
# Pretty JSON for display, compact JSON for URL
|
|
303
|
+
manifest_pretty = json.dumps(manifest, indent=2)
|
|
304
|
+
manifest_compact = json.dumps(manifest, separators=(',', ':'))
|
|
305
|
+
return {"ok": True, "manifest": manifest_pretty, "manifest_compact": manifest_compact}
|
|
306
|
+
except Exception as exc:
|
|
307
|
+
logger.error("Failed to load Slack manifest: %s", exc)
|
|
308
|
+
return {"ok": False, "error": str(exc)}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_version_info() -> dict:
|
|
312
|
+
"""Get current version and check for updates.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
{
|
|
316
|
+
"current": str,
|
|
317
|
+
"latest": str | None,
|
|
318
|
+
"has_update": bool,
|
|
319
|
+
"error": str | None
|
|
320
|
+
}
|
|
321
|
+
"""
|
|
322
|
+
import urllib.request
|
|
323
|
+
from vibe import __version__
|
|
324
|
+
|
|
325
|
+
current = __version__
|
|
326
|
+
result = {"current": current, "latest": None, "has_update": False, "error": None}
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
url = "https://pypi.org/pypi/vibe-remote/json"
|
|
330
|
+
req = urllib.request.Request(url, headers={"User-Agent": "vibe-remote"})
|
|
331
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
332
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
333
|
+
latest = data.get("info", {}).get("version", "")
|
|
334
|
+
result["latest"] = latest
|
|
335
|
+
|
|
336
|
+
# Simple version comparison (works for semver)
|
|
337
|
+
if latest and latest != current:
|
|
338
|
+
try:
|
|
339
|
+
current_parts = [int(x) for x in current.split(".")[:3] if x.isdigit()]
|
|
340
|
+
latest_parts = [int(x) for x in latest.split(".")[:3] if x.isdigit()]
|
|
341
|
+
result["has_update"] = latest_parts > current_parts
|
|
342
|
+
except (ValueError, AttributeError):
|
|
343
|
+
result["has_update"] = latest != current
|
|
344
|
+
except Exception as e:
|
|
345
|
+
result["error"] = str(e)
|
|
346
|
+
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def do_upgrade(auto_restart: bool = True) -> dict:
|
|
351
|
+
"""Perform upgrade to latest version.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
auto_restart: If True, restart vibe after successful upgrade
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
{"ok": bool, "message": str, "output": str | None, "restarting": bool}
|
|
358
|
+
"""
|
|
359
|
+
import sys
|
|
360
|
+
|
|
361
|
+
# Determine upgrade method based on how vibe was installed
|
|
362
|
+
# Check if running from uv tool environment
|
|
363
|
+
exe_path = sys.executable
|
|
364
|
+
is_uv_tool = ".local/share/uv/tools/" in exe_path or "/uv/tools/" in exe_path
|
|
365
|
+
|
|
366
|
+
uv_path = shutil.which("uv")
|
|
367
|
+
|
|
368
|
+
if is_uv_tool and uv_path:
|
|
369
|
+
# Installed via uv tool, upgrade with uv
|
|
370
|
+
cmd = [uv_path, "tool", "upgrade", "vibe-remote"]
|
|
371
|
+
else:
|
|
372
|
+
# Installed via pip or other method, use current Python's pip
|
|
373
|
+
cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "vibe-remote"]
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
377
|
+
if result.returncode == 0:
|
|
378
|
+
restarting = False
|
|
379
|
+
if auto_restart:
|
|
380
|
+
# Schedule restart in background after response is sent
|
|
381
|
+
# Use 'vibe' command which will restart both service and UI
|
|
382
|
+
vibe_path = shutil.which("vibe")
|
|
383
|
+
if vibe_path:
|
|
384
|
+
# Start restart process detached, with delay to allow response to be sent
|
|
385
|
+
restart_cmd = f"sleep 2 && {vibe_path}"
|
|
386
|
+
subprocess.Popen(
|
|
387
|
+
restart_cmd,
|
|
388
|
+
shell=True,
|
|
389
|
+
stdout=subprocess.DEVNULL,
|
|
390
|
+
stderr=subprocess.DEVNULL,
|
|
391
|
+
start_new_session=True,
|
|
392
|
+
)
|
|
393
|
+
restarting = True
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
"ok": True,
|
|
397
|
+
"message": "Upgrade successful." + (" Restarting..." if restarting else " Please restart vibe."),
|
|
398
|
+
"output": result.stdout,
|
|
399
|
+
"restarting": restarting,
|
|
400
|
+
}
|
|
401
|
+
else:
|
|
402
|
+
return {
|
|
403
|
+
"ok": False,
|
|
404
|
+
"message": "Upgrade failed",
|
|
405
|
+
"output": result.stderr or result.stdout,
|
|
406
|
+
"restarting": False,
|
|
407
|
+
}
|
|
408
|
+
except subprocess.TimeoutExpired:
|
|
409
|
+
return {"ok": False, "message": "Upgrade timed out", "output": None, "restarting": False}
|
|
410
|
+
except Exception as e:
|
|
411
|
+
return {"ok": False, "message": str(e), "output": None, "restarting": False}
|
|
412
|
+
|