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.
Files changed (52) hide show
  1. config/__init__.py +37 -0
  2. config/paths.py +56 -0
  3. config/v2_compat.py +74 -0
  4. config/v2_config.py +206 -0
  5. config/v2_sessions.py +73 -0
  6. config/v2_settings.py +115 -0
  7. core/__init__.py +0 -0
  8. core/controller.py +736 -0
  9. core/handlers/__init__.py +13 -0
  10. core/handlers/command_handlers.py +342 -0
  11. core/handlers/message_handler.py +365 -0
  12. core/handlers/session_handler.py +233 -0
  13. core/handlers/settings_handler.py +362 -0
  14. modules/__init__.py +0 -0
  15. modules/agent_router.py +58 -0
  16. modules/agents/__init__.py +38 -0
  17. modules/agents/base.py +91 -0
  18. modules/agents/claude_agent.py +344 -0
  19. modules/agents/codex_agent.py +368 -0
  20. modules/agents/opencode_agent.py +2155 -0
  21. modules/agents/service.py +41 -0
  22. modules/agents/subagent_router.py +136 -0
  23. modules/claude_client.py +154 -0
  24. modules/im/__init__.py +63 -0
  25. modules/im/base.py +323 -0
  26. modules/im/factory.py +60 -0
  27. modules/im/formatters/__init__.py +4 -0
  28. modules/im/formatters/base_formatter.py +639 -0
  29. modules/im/formatters/slack_formatter.py +127 -0
  30. modules/im/slack.py +2091 -0
  31. modules/session_manager.py +138 -0
  32. modules/settings_manager.py +587 -0
  33. vibe/__init__.py +6 -0
  34. vibe/__main__.py +12 -0
  35. vibe/_version.py +34 -0
  36. vibe/api.py +412 -0
  37. vibe/cli.py +637 -0
  38. vibe/runtime.py +213 -0
  39. vibe/service_main.py +101 -0
  40. vibe/templates/slack_manifest.json +65 -0
  41. vibe/ui/dist/assets/index-8g3mNwMK.js +35 -0
  42. vibe/ui/dist/assets/index-M55aMB5R.css +1 -0
  43. vibe/ui/dist/assets/logo-BzryTZ7u.png +0 -0
  44. vibe/ui/dist/index.html +17 -0
  45. vibe/ui/dist/logo.png +0 -0
  46. vibe/ui/dist/vite.svg +1 -0
  47. vibe/ui_server.py +346 -0
  48. vibe_remote-2.1.6.dist-info/METADATA +295 -0
  49. vibe_remote-2.1.6.dist-info/RECORD +52 -0
  50. vibe_remote-2.1.6.dist-info/WHEEL +4 -0
  51. vibe_remote-2.1.6.dist-info/entry_points.txt +2 -0
  52. 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
@@ -0,0 +1,6 @@
1
+ """Vibe Remote - Local-first agent runtime for Slack"""
2
+
3
+ try:
4
+ from vibe._version import __version__
5
+ except ImportError:
6
+ __version__ = "0.0.0.dev0" # Fallback for editable installs without build
vibe/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ import sys
2
+
3
+
4
+ if sys.version_info < (3, 9):
5
+ sys.stderr.write("Vibe requires Python 3.9+. Please use python3 -m vibe.\n")
6
+ sys.exit(1)
7
+
8
+ from vibe.cli import main
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
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