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
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import hashlib
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
from typing import Any, Dict, List, Optional, Union
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from config import paths
|
|
10
|
+
from config.v2_sessions import SessionsStore
|
|
11
|
+
from config.v2_settings import SettingsStore, ChannelSettings, RoutingSettings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DEFAULT_SHOW_MESSAGE_TYPES: List[str] = []
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ChannelRouting:
|
|
22
|
+
"""Per-channel agent routing configuration."""
|
|
23
|
+
|
|
24
|
+
agent_backend: Optional[str] = None # "claude" | "codex" | "opencode" | None
|
|
25
|
+
opencode_agent: Optional[str] = None # "build" | "plan" | ... | None
|
|
26
|
+
opencode_model: Optional[str] = None # "provider/model" | None
|
|
27
|
+
opencode_reasoning_effort: Optional[str] = None # "low" | "medium" | "high" | "xhigh" | None
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict:
|
|
30
|
+
"""Convert to dictionary for JSON serialization"""
|
|
31
|
+
return asdict(self)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, data: dict) -> "ChannelRouting":
|
|
35
|
+
"""Create from dictionary"""
|
|
36
|
+
if data is None:
|
|
37
|
+
return None
|
|
38
|
+
return cls(
|
|
39
|
+
agent_backend=data.get("agent_backend"),
|
|
40
|
+
opencode_agent=data.get("opencode_agent"),
|
|
41
|
+
opencode_model=data.get("opencode_model"),
|
|
42
|
+
opencode_reasoning_effort=data.get("opencode_reasoning_effort"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class UserSettings:
|
|
48
|
+
show_message_types: List[str] = field(
|
|
49
|
+
default_factory=lambda: DEFAULT_SHOW_MESSAGE_TYPES.copy()
|
|
50
|
+
)
|
|
51
|
+
custom_cwd: Optional[str] = None
|
|
52
|
+
channel_routing: Optional[ChannelRouting] = None
|
|
53
|
+
enabled: bool = True
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict:
|
|
56
|
+
"""Convert to dictionary for JSON serialization"""
|
|
57
|
+
result = {
|
|
58
|
+
"show_message_types": self.show_message_types,
|
|
59
|
+
"custom_cwd": self.custom_cwd,
|
|
60
|
+
}
|
|
61
|
+
if self.channel_routing is not None:
|
|
62
|
+
result["routing"] = self.channel_routing.to_dict()
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_dict(cls, data: dict) -> "UserSettings":
|
|
67
|
+
"""Create from dictionary"""
|
|
68
|
+
if data is None:
|
|
69
|
+
return cls()
|
|
70
|
+
payload = dict(data)
|
|
71
|
+
routing_data = payload.pop("routing", None)
|
|
72
|
+
show_message_types = payload.get("show_message_types")
|
|
73
|
+
settings = cls(
|
|
74
|
+
show_message_types=(
|
|
75
|
+
show_message_types
|
|
76
|
+
if show_message_types is not None
|
|
77
|
+
else DEFAULT_SHOW_MESSAGE_TYPES.copy()
|
|
78
|
+
),
|
|
79
|
+
custom_cwd=payload.get("custom_cwd"),
|
|
80
|
+
)
|
|
81
|
+
if routing_data is not None:
|
|
82
|
+
settings.channel_routing = ChannelRouting.from_dict(routing_data)
|
|
83
|
+
return settings
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class SettingsManager:
|
|
87
|
+
"""Manages user personalization settings with JSON persistence"""
|
|
88
|
+
|
|
89
|
+
MESSAGE_TYPE_ALIASES = {
|
|
90
|
+
"tool_call": "toolcall",
|
|
91
|
+
"tool": "toolcall",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def __init__(self, settings_file: Optional[str] = None):
|
|
95
|
+
paths.ensure_data_dirs()
|
|
96
|
+
self.settings_file = Path(settings_file) if settings_file else paths.get_settings_path()
|
|
97
|
+
self._settings_mtime_ns: Optional[int] = None
|
|
98
|
+
self._settings_fingerprint: Optional[str] = None
|
|
99
|
+
self.settings: Dict[Union[int, str], UserSettings] = {}
|
|
100
|
+
self.store = SettingsStore(self.settings_file)
|
|
101
|
+
self.sessions_store = SessionsStore()
|
|
102
|
+
self.sessions_store.load()
|
|
103
|
+
self._load_settings()
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------
|
|
106
|
+
# Internal helpers
|
|
107
|
+
# ---------------------------------------------
|
|
108
|
+
def _normalize_user_id(self, user_id: Union[int, str]) -> str:
|
|
109
|
+
"""Normalize user_id consistently to string.
|
|
110
|
+
|
|
111
|
+
Rationale: JSON object keys are strings; Slack IDs are strings; unifying to
|
|
112
|
+
string avoids mixed-type keys (e.g., 123 vs "123").
|
|
113
|
+
"""
|
|
114
|
+
return str(user_id)
|
|
115
|
+
|
|
116
|
+
def _from_channel_settings(self, channel_settings: ChannelSettings) -> UserSettings:
|
|
117
|
+
routing = ChannelRouting(
|
|
118
|
+
agent_backend=channel_settings.routing.agent_backend,
|
|
119
|
+
opencode_agent=channel_settings.routing.opencode_agent,
|
|
120
|
+
opencode_model=channel_settings.routing.opencode_model,
|
|
121
|
+
opencode_reasoning_effort=channel_settings.routing.opencode_reasoning_effort,
|
|
122
|
+
)
|
|
123
|
+
return UserSettings(
|
|
124
|
+
show_message_types=self._normalize_show_message_types(
|
|
125
|
+
channel_settings.show_message_types
|
|
126
|
+
),
|
|
127
|
+
custom_cwd=channel_settings.custom_cwd,
|
|
128
|
+
channel_routing=routing,
|
|
129
|
+
enabled=channel_settings.enabled,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _to_channel_settings(self, settings: UserSettings) -> ChannelSettings:
|
|
133
|
+
routing = settings.channel_routing or ChannelRouting()
|
|
134
|
+
return ChannelSettings(
|
|
135
|
+
enabled=settings.enabled,
|
|
136
|
+
show_message_types=self._normalize_show_message_types(
|
|
137
|
+
settings.show_message_types
|
|
138
|
+
),
|
|
139
|
+
custom_cwd=settings.custom_cwd,
|
|
140
|
+
routing=RoutingSettings(
|
|
141
|
+
agent_backend=routing.agent_backend,
|
|
142
|
+
opencode_agent=routing.opencode_agent,
|
|
143
|
+
opencode_model=routing.opencode_model,
|
|
144
|
+
opencode_reasoning_effort=routing.opencode_reasoning_effort,
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def _load_settings(self):
|
|
149
|
+
"""Load settings from JSON file"""
|
|
150
|
+
self.store = SettingsStore(self.settings_file)
|
|
151
|
+
self.settings = {}
|
|
152
|
+
|
|
153
|
+
if not self.store.settings.channels:
|
|
154
|
+
logger.info("No settings file found, starting with empty settings")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
for channel_id, channel_settings in self.store.settings.channels.items():
|
|
158
|
+
self.settings[str(channel_id)] = self._from_channel_settings(channel_settings)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
self._settings_mtime_ns = self.settings_file.stat().st_mtime_ns
|
|
162
|
+
self._settings_fingerprint = self._compute_settings_fingerprint()
|
|
163
|
+
except FileNotFoundError:
|
|
164
|
+
self._settings_mtime_ns = None
|
|
165
|
+
self._settings_fingerprint = None
|
|
166
|
+
|
|
167
|
+
logger.info(f"Loaded settings for {len(self.settings)} channels")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _compute_settings_fingerprint(self) -> Optional[str]:
|
|
171
|
+
try:
|
|
172
|
+
data = self.settings_file.read_bytes()
|
|
173
|
+
except FileNotFoundError:
|
|
174
|
+
return None
|
|
175
|
+
return hashlib.sha256(data).hexdigest()
|
|
176
|
+
|
|
177
|
+
def _reload_if_changed(self) -> None:
|
|
178
|
+
if not self.settings_file.exists():
|
|
179
|
+
return
|
|
180
|
+
try:
|
|
181
|
+
mtime_ns = self.settings_file.stat().st_mtime_ns
|
|
182
|
+
except FileNotFoundError:
|
|
183
|
+
return
|
|
184
|
+
fingerprint = None
|
|
185
|
+
if self._settings_mtime_ns is None or mtime_ns != self._settings_mtime_ns:
|
|
186
|
+
fingerprint = self._compute_settings_fingerprint()
|
|
187
|
+
elif self._settings_fingerprint is None:
|
|
188
|
+
fingerprint = self._compute_settings_fingerprint()
|
|
189
|
+
if fingerprint and fingerprint != self._settings_fingerprint:
|
|
190
|
+
logger.info("Settings file changed on disk, reloading")
|
|
191
|
+
self._load_settings()
|
|
192
|
+
elif fingerprint:
|
|
193
|
+
self._settings_fingerprint = fingerprint
|
|
194
|
+
self._settings_mtime_ns = mtime_ns
|
|
195
|
+
else:
|
|
196
|
+
self._settings_mtime_ns = mtime_ns
|
|
197
|
+
|
|
198
|
+
def _save_settings(self):
|
|
199
|
+
"""Save settings to JSON file"""
|
|
200
|
+
try:
|
|
201
|
+
channels: Dict[str, ChannelSettings] = {}
|
|
202
|
+
for settings_key, settings in self.settings.items():
|
|
203
|
+
existing = self.store.settings.channels.get(str(settings_key))
|
|
204
|
+
channel_settings = self._to_channel_settings(settings)
|
|
205
|
+
if existing is not None:
|
|
206
|
+
channel_settings.enabled = existing.enabled
|
|
207
|
+
channels[str(settings_key)] = channel_settings
|
|
208
|
+
self.store.settings.channels = channels
|
|
209
|
+
self.store.save()
|
|
210
|
+
try:
|
|
211
|
+
self._settings_mtime_ns = self.settings_file.stat().st_mtime_ns
|
|
212
|
+
self._settings_fingerprint = self._compute_settings_fingerprint()
|
|
213
|
+
except FileNotFoundError:
|
|
214
|
+
self._settings_mtime_ns = None
|
|
215
|
+
self._settings_fingerprint = None
|
|
216
|
+
logger.info("Settings saved successfully")
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(f"Error saving settings: {e}")
|
|
219
|
+
|
|
220
|
+
def get_user_settings(self, user_id: Union[int, str]) -> UserSettings:
|
|
221
|
+
"""Get settings for a specific user"""
|
|
222
|
+
normalized_id = self._normalize_user_id(user_id)
|
|
223
|
+
|
|
224
|
+
self._reload_if_changed()
|
|
225
|
+
|
|
226
|
+
# Return existing or create new
|
|
227
|
+
if normalized_id not in self.settings:
|
|
228
|
+
settings = UserSettings()
|
|
229
|
+
if normalized_id in self.store.settings.channels:
|
|
230
|
+
settings = self._from_channel_settings(
|
|
231
|
+
self.store.settings.channels[normalized_id]
|
|
232
|
+
)
|
|
233
|
+
self.settings[normalized_id] = settings
|
|
234
|
+
self._save_settings()
|
|
235
|
+
return self.settings[normalized_id]
|
|
236
|
+
|
|
237
|
+
def update_user_settings(self, user_id: Union[int, str], settings: UserSettings):
|
|
238
|
+
"""Update settings for a specific user"""
|
|
239
|
+
normalized_id = self._normalize_user_id(user_id)
|
|
240
|
+
|
|
241
|
+
settings.show_message_types = self._normalize_show_message_types(
|
|
242
|
+
settings.show_message_types
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
self.settings[normalized_id] = settings
|
|
246
|
+
self._save_settings()
|
|
247
|
+
|
|
248
|
+
def toggle_show_message_type(
|
|
249
|
+
self, user_id: Union[int, str], message_type: str
|
|
250
|
+
) -> bool:
|
|
251
|
+
"""Toggle a message type in show list, returns new state (True if now shown)"""
|
|
252
|
+
message_type = self._canonicalize_message_type(message_type)
|
|
253
|
+
settings = self.get_user_settings(user_id)
|
|
254
|
+
|
|
255
|
+
if message_type in settings.show_message_types:
|
|
256
|
+
settings.show_message_types.remove(message_type)
|
|
257
|
+
is_shown = False
|
|
258
|
+
else:
|
|
259
|
+
settings.show_message_types.append(message_type)
|
|
260
|
+
is_shown = True
|
|
261
|
+
|
|
262
|
+
self.update_user_settings(user_id, settings)
|
|
263
|
+
return is_shown
|
|
264
|
+
|
|
265
|
+
def set_custom_cwd(self, user_id: Union[int, str], cwd: str):
|
|
266
|
+
"""Set custom working directory for user"""
|
|
267
|
+
settings = self.get_user_settings(user_id)
|
|
268
|
+
settings.custom_cwd = cwd
|
|
269
|
+
self.update_user_settings(user_id, settings)
|
|
270
|
+
|
|
271
|
+
def get_custom_cwd(self, user_id: Union[int, str]) -> Optional[str]:
|
|
272
|
+
"""Get custom working directory for user"""
|
|
273
|
+
settings = self.get_user_settings(user_id)
|
|
274
|
+
return settings.custom_cwd
|
|
275
|
+
|
|
276
|
+
def get_channel_settings(self, channel_id: Union[int, str]) -> Optional[ChannelSettings]:
|
|
277
|
+
"""Get raw ChannelSettings for a channel without creating defaults."""
|
|
278
|
+
self._reload_if_changed()
|
|
279
|
+
key = str(channel_id)
|
|
280
|
+
return self.store.settings.channels.get(key)
|
|
281
|
+
|
|
282
|
+
def is_message_type_hidden(
|
|
283
|
+
self, user_id: Union[int, str], message_type: str
|
|
284
|
+
) -> bool:
|
|
285
|
+
"""Check if a message type is hidden for user (not in show_message_types)"""
|
|
286
|
+
self._reload_if_changed()
|
|
287
|
+
message_type = self._canonicalize_message_type(message_type)
|
|
288
|
+
settings = self.get_user_settings(user_id)
|
|
289
|
+
return message_type not in settings.show_message_types
|
|
290
|
+
|
|
291
|
+
def save_user_settings(self, user_id: Union[int, str], settings: UserSettings):
|
|
292
|
+
"""Save settings for a specific user (alias for update_user_settings)"""
|
|
293
|
+
self.update_user_settings(user_id, settings)
|
|
294
|
+
|
|
295
|
+
def get_available_message_types(self) -> List[str]:
|
|
296
|
+
"""Get list of available message types that can be hidden"""
|
|
297
|
+
return ["system", "assistant", "toolcall"]
|
|
298
|
+
|
|
299
|
+
def get_message_type_display_names(self) -> Dict[str, str]:
|
|
300
|
+
"""Get display names for message types"""
|
|
301
|
+
return {
|
|
302
|
+
"system": "System",
|
|
303
|
+
"assistant": "Assistant",
|
|
304
|
+
"toolcall": "Toolcall",
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def _ensure_agent_namespace(self, user_id: Union[int, str], agent_name: str) -> Dict[str, str]:
|
|
308
|
+
user_key = self._normalize_user_id(user_id)
|
|
309
|
+
return self.sessions_store.get_agent_map(user_key, agent_name)
|
|
310
|
+
|
|
311
|
+
def set_agent_session_mapping(
|
|
312
|
+
self,
|
|
313
|
+
user_id: Union[int, str],
|
|
314
|
+
agent_name: str,
|
|
315
|
+
thread_id: str,
|
|
316
|
+
session_id: str,
|
|
317
|
+
):
|
|
318
|
+
"""Store mapping between thread ID and agent session ID"""
|
|
319
|
+
agent_map = self._ensure_agent_namespace(user_id, agent_name)
|
|
320
|
+
agent_map[thread_id] = session_id
|
|
321
|
+
self.sessions_store.save()
|
|
322
|
+
logger.info(
|
|
323
|
+
f"Stored {agent_name} session mapping for {user_id}: "
|
|
324
|
+
f"{thread_id} -> {session_id}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
def get_agent_session_id(
|
|
328
|
+
self,
|
|
329
|
+
user_id: Union[int, str],
|
|
330
|
+
thread_id: str,
|
|
331
|
+
agent_name: str,
|
|
332
|
+
) -> Optional[str]:
|
|
333
|
+
"""Get agent session ID for given thread ID"""
|
|
334
|
+
user_key = self._normalize_user_id(user_id)
|
|
335
|
+
agent_map = self.sessions_store.get_agent_map(user_key, agent_name)
|
|
336
|
+
return agent_map.get(thread_id)
|
|
337
|
+
|
|
338
|
+
def _canonicalize_message_type(self, message_type: str) -> str:
|
|
339
|
+
"""Normalize message type to canonical form to support aliases."""
|
|
340
|
+
return self.MESSAGE_TYPE_ALIASES.get(message_type, message_type)
|
|
341
|
+
|
|
342
|
+
def _normalize_show_message_types(self, show_message_types: Optional[List[str]]) -> List[str]:
|
|
343
|
+
"""Normalize and migrate show message types to current canonical schema."""
|
|
344
|
+
allowed = {"system", "assistant", "toolcall"}
|
|
345
|
+
if show_message_types is None:
|
|
346
|
+
return DEFAULT_SHOW_MESSAGE_TYPES.copy()
|
|
347
|
+
normalized: List[str] = []
|
|
348
|
+
seen = set()
|
|
349
|
+
|
|
350
|
+
for msg_type in show_message_types or []:
|
|
351
|
+
canonical = self._canonicalize_message_type(msg_type)
|
|
352
|
+
if canonical not in allowed:
|
|
353
|
+
continue
|
|
354
|
+
if canonical in seen:
|
|
355
|
+
continue
|
|
356
|
+
seen.add(canonical)
|
|
357
|
+
normalized.append(canonical)
|
|
358
|
+
|
|
359
|
+
return normalized
|
|
360
|
+
|
|
361
|
+
def clear_agent_session_mapping(
|
|
362
|
+
self,
|
|
363
|
+
user_id: Union[int, str],
|
|
364
|
+
agent_name: str,
|
|
365
|
+
thread_id: str,
|
|
366
|
+
):
|
|
367
|
+
"""Clear session mapping for given thread ID"""
|
|
368
|
+
user_key = self._normalize_user_id(user_id)
|
|
369
|
+
agent_map = self.sessions_store.get_agent_map(user_key, agent_name)
|
|
370
|
+
if thread_id in agent_map:
|
|
371
|
+
del agent_map[thread_id]
|
|
372
|
+
logger.info(
|
|
373
|
+
f"Cleared {agent_name} session mapping for user {user_id}: {thread_id}"
|
|
374
|
+
)
|
|
375
|
+
self.sessions_store.save()
|
|
376
|
+
|
|
377
|
+
def clear_agent_sessions(self, user_id: Union[int, str], agent_name: str):
|
|
378
|
+
"""Clear every session mapping for the specified agent."""
|
|
379
|
+
user_key = self._normalize_user_id(user_id)
|
|
380
|
+
agent_map = self.sessions_store.get_agent_map(user_key, agent_name)
|
|
381
|
+
if agent_map:
|
|
382
|
+
self.sessions_store.state.session_mappings[user_key][agent_name] = {}
|
|
383
|
+
logger.info(
|
|
384
|
+
f"Cleared all {agent_name} session namespaces for user {user_id}"
|
|
385
|
+
)
|
|
386
|
+
self.sessions_store.save()
|
|
387
|
+
|
|
388
|
+
def clear_all_session_mappings(self, user_id: Union[int, str]):
|
|
389
|
+
"""Clear all session mappings for a user across agents"""
|
|
390
|
+
user_key = self._normalize_user_id(user_id)
|
|
391
|
+
agent_maps = self.sessions_store.state.session_mappings.get(user_key, {})
|
|
392
|
+
if agent_maps:
|
|
393
|
+
count = sum(len(agent_map) for agent_map in agent_maps.values())
|
|
394
|
+
self.sessions_store.state.session_mappings[user_key] = {}
|
|
395
|
+
logger.info(
|
|
396
|
+
f"Cleared all session mappings ({count} bases) for user {user_id}"
|
|
397
|
+
)
|
|
398
|
+
self.sessions_store.save()
|
|
399
|
+
|
|
400
|
+
def list_agent_sessions(
|
|
401
|
+
self, user_id: Union[int, str], agent_name: str
|
|
402
|
+
) -> Dict[str, str]:
|
|
403
|
+
"""Get copy of session mappings (thread_id -> session_id) for an agent."""
|
|
404
|
+
user_key = self._normalize_user_id(user_id)
|
|
405
|
+
agent_map = self.sessions_store.get_agent_map(user_key, agent_name)
|
|
406
|
+
return dict(agent_map)
|
|
407
|
+
|
|
408
|
+
# Backwards-compatible helpers for Claude-specific call sites
|
|
409
|
+
def set_session_mapping(
|
|
410
|
+
self,
|
|
411
|
+
user_id: Union[int, str],
|
|
412
|
+
thread_id: str,
|
|
413
|
+
claude_session_id: str,
|
|
414
|
+
):
|
|
415
|
+
self.set_agent_session_mapping(
|
|
416
|
+
user_id, "claude", thread_id, claude_session_id
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def get_claude_session_id(
|
|
420
|
+
self, user_id: Union[int, str], thread_id: str
|
|
421
|
+
) -> Optional[str]:
|
|
422
|
+
return self.get_agent_session_id(
|
|
423
|
+
user_id, thread_id, agent_name="claude"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def clear_session_mapping(
|
|
427
|
+
self,
|
|
428
|
+
user_id: Union[int, str],
|
|
429
|
+
thread_id: str,
|
|
430
|
+
):
|
|
431
|
+
self.clear_agent_session_mapping(
|
|
432
|
+
user_id, "claude", thread_id
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# ---------------------------------------------
|
|
436
|
+
# Slack thread management
|
|
437
|
+
# ---------------------------------------------
|
|
438
|
+
def mark_thread_active(
|
|
439
|
+
self, user_id: Union[int, str], channel_id: str, thread_ts: str
|
|
440
|
+
):
|
|
441
|
+
"""Mark a Slack thread as active with current timestamp"""
|
|
442
|
+
user_key = self._normalize_user_id(user_id)
|
|
443
|
+
channel_map = self.sessions_store.get_thread_map(user_key, channel_id)
|
|
444
|
+
channel_map[thread_ts] = time.time()
|
|
445
|
+
self.sessions_store.save()
|
|
446
|
+
logger.info(
|
|
447
|
+
f"Marked thread active for user {user_id}: channel={channel_id}, thread={thread_ts}"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
def is_thread_active(
|
|
451
|
+
self, user_id: Union[int, str], channel_id: str, thread_ts: str
|
|
452
|
+
) -> bool:
|
|
453
|
+
"""Check if a Slack thread is active (within 24 hours)"""
|
|
454
|
+
user_key = self._normalize_user_id(user_id)
|
|
455
|
+
|
|
456
|
+
# First cleanup expired threads for this channel
|
|
457
|
+
self._cleanup_expired_threads_for_channel(user_id, channel_id)
|
|
458
|
+
|
|
459
|
+
channel_map = self.sessions_store.get_thread_map(user_key, channel_id)
|
|
460
|
+
return thread_ts in channel_map
|
|
461
|
+
|
|
462
|
+
def _cleanup_expired_threads_for_channel(
|
|
463
|
+
self, user_id: Union[int, str], channel_id: str
|
|
464
|
+
):
|
|
465
|
+
"""Remove threads older than 24 hours for a specific channel"""
|
|
466
|
+
user_key = self._normalize_user_id(user_id)
|
|
467
|
+
channel_map = self.sessions_store.get_thread_map(user_key, channel_id)
|
|
468
|
+
|
|
469
|
+
if not channel_map:
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
current_time = time.time()
|
|
473
|
+
twenty_four_hours_ago = current_time - (24 * 60 * 60)
|
|
474
|
+
|
|
475
|
+
expired_threads = [
|
|
476
|
+
thread_ts
|
|
477
|
+
for thread_ts, last_active in channel_map.items()
|
|
478
|
+
if last_active < twenty_four_hours_ago
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
if expired_threads:
|
|
482
|
+
for thread_ts in expired_threads:
|
|
483
|
+
del channel_map[thread_ts]
|
|
484
|
+
|
|
485
|
+
if not channel_map:
|
|
486
|
+
self.sessions_store.state.active_slack_threads[user_key].pop(channel_id, None)
|
|
487
|
+
|
|
488
|
+
self.sessions_store.save()
|
|
489
|
+
logger.info(
|
|
490
|
+
f"Cleaned up {len(expired_threads)} expired threads for channel {channel_id}"
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
def cleanup_all_expired_threads(self, user_id: Union[int, str]):
|
|
494
|
+
"""Remove all threads older than 24 hours for all channels"""
|
|
495
|
+
user_key = self._normalize_user_id(user_id)
|
|
496
|
+
channel_map = self.sessions_store.state.active_slack_threads.get(user_key, {})
|
|
497
|
+
|
|
498
|
+
if not channel_map:
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
channels_to_clean = list(channel_map.keys())
|
|
502
|
+
for channel_id in channels_to_clean:
|
|
503
|
+
self._cleanup_expired_threads_for_channel(user_id, channel_id)
|
|
504
|
+
|
|
505
|
+
# ---------------------------------------------
|
|
506
|
+
# Channel routing management
|
|
507
|
+
# ---------------------------------------------
|
|
508
|
+
def get_channel_routing(
|
|
509
|
+
self, settings_key: Union[int, str]
|
|
510
|
+
) -> Optional[ChannelRouting]:
|
|
511
|
+
"""Get channel routing override for the given settings key."""
|
|
512
|
+
self._reload_if_changed()
|
|
513
|
+
settings = self.get_user_settings(settings_key)
|
|
514
|
+
return settings.channel_routing
|
|
515
|
+
|
|
516
|
+
def set_channel_routing(
|
|
517
|
+
self, settings_key: Union[int, str], routing: ChannelRouting
|
|
518
|
+
):
|
|
519
|
+
"""Set channel routing override."""
|
|
520
|
+
settings = self.get_user_settings(settings_key)
|
|
521
|
+
settings.channel_routing = routing
|
|
522
|
+
self.update_user_settings(settings_key, settings)
|
|
523
|
+
logger.info(
|
|
524
|
+
f"Updated channel routing for {settings_key}: "
|
|
525
|
+
f"backend={routing.agent_backend}, "
|
|
526
|
+
f"opencode_agent={routing.opencode_agent}, "
|
|
527
|
+
f"opencode_model={routing.opencode_model}"
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
def clear_channel_routing(self, settings_key: Union[int, str]):
|
|
531
|
+
"""Clear channel routing override (fall back to default backend)."""
|
|
532
|
+
settings = self.get_user_settings(settings_key)
|
|
533
|
+
if settings.channel_routing:
|
|
534
|
+
settings.channel_routing = None
|
|
535
|
+
self.update_user_settings(settings_key, settings)
|
|
536
|
+
logger.info(f"Cleared channel routing for {settings_key}")
|
|
537
|
+
|
|
538
|
+
# ---------------------------------------------
|
|
539
|
+
# Per-channel require_mention management
|
|
540
|
+
# ---------------------------------------------
|
|
541
|
+
def get_require_mention(
|
|
542
|
+
self, channel_id: Union[int, str], global_default: bool = False
|
|
543
|
+
) -> bool:
|
|
544
|
+
"""Get effective require_mention value for a channel.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
channel_id: The channel to check
|
|
548
|
+
global_default: The global require_mention setting from config
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
True if mention is required, False otherwise.
|
|
552
|
+
Uses per-channel setting if set, otherwise falls back to global_default.
|
|
553
|
+
"""
|
|
554
|
+
self._reload_if_changed()
|
|
555
|
+
key = str(channel_id)
|
|
556
|
+
channel_settings = self.store.settings.channels.get(key)
|
|
557
|
+
|
|
558
|
+
if channel_settings is not None and channel_settings.require_mention is not None:
|
|
559
|
+
return channel_settings.require_mention
|
|
560
|
+
|
|
561
|
+
return global_default
|
|
562
|
+
|
|
563
|
+
def set_require_mention(
|
|
564
|
+
self, channel_id: Union[int, str], value: Optional[bool]
|
|
565
|
+
):
|
|
566
|
+
"""Set per-channel require_mention override.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
channel_id: The channel to configure
|
|
570
|
+
value: True=require mention, False=don't require, None=use global default
|
|
571
|
+
"""
|
|
572
|
+
key = str(channel_id)
|
|
573
|
+
channel_settings = self.store.get_channel(key)
|
|
574
|
+
channel_settings.require_mention = value
|
|
575
|
+
self.store.update_channel(key, channel_settings)
|
|
576
|
+
logger.info(f"Updated require_mention for channel {key}: {value}")
|
|
577
|
+
|
|
578
|
+
def get_require_mention_override(
|
|
579
|
+
self, channel_id: Union[int, str]
|
|
580
|
+
) -> Optional[bool]:
|
|
581
|
+
"""Get the raw per-channel require_mention override (may be None)."""
|
|
582
|
+
self._reload_if_changed()
|
|
583
|
+
key = str(channel_id)
|
|
584
|
+
channel_settings = self.store.settings.channels.get(key)
|
|
585
|
+
if channel_settings is not None:
|
|
586
|
+
return channel_settings.require_mention
|
|
587
|
+
return None
|
vibe/__init__.py
ADDED
vibe/__main__.py
ADDED
vibe/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '2.1.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 1, 6)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|