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,362 @@
1
+ """Settings and configuration handlers"""
2
+
3
+ import logging
4
+ from modules.agents import get_agent_display_name
5
+ from modules.im import MessageContext, InlineKeyboard, InlineButton
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class SettingsHandler:
11
+ """Handles settings and configuration operations"""
12
+
13
+ def __init__(self, controller):
14
+ """Initialize with reference to main controller"""
15
+ self.controller = controller
16
+ self.config = controller.config
17
+ self.im_client = controller.im_client
18
+ self.settings_manager = controller.settings_manager
19
+ self.formatter = controller.im_client.formatter
20
+
21
+ def _get_settings_key(self, context: MessageContext) -> str:
22
+ """Get settings key - delegate to controller"""
23
+ return self.controller._get_settings_key(context)
24
+
25
+ def _get_agent_display_name(self, context: MessageContext) -> str:
26
+ """Return a friendly agent name for the current context."""
27
+ agent_name = self.controller.resolve_agent_for_context(context)
28
+ default_agent = getattr(self.controller.agent_service, "default_agent", None)
29
+ return get_agent_display_name(agent_name, fallback=default_agent)
30
+
31
+ async def handle_settings(self, context: MessageContext, args: str = ""):
32
+ """Handle settings command - show settings menu"""
33
+ try:
34
+ # For Slack, use modal dialog
35
+ if self.config.platform == "slack":
36
+ await self._handle_settings_slack(context)
37
+ else:
38
+ # For other platforms, use inline keyboard
39
+ await self._handle_settings_traditional(context)
40
+
41
+ except Exception as e:
42
+ logger.error(f"Error showing settings: {e}")
43
+ await self.im_client.send_message(
44
+ context, f"❌ Error showing settings: {str(e)}"
45
+ )
46
+
47
+ async def _handle_settings_traditional(self, context: MessageContext):
48
+ """Handle settings for non-Slack platforms"""
49
+ # Get current settings
50
+ settings_key = self._get_settings_key(context)
51
+ user_settings = self.settings_manager.get_user_settings(settings_key)
52
+
53
+ # Get available message types and display names
54
+ message_types = self.settings_manager.get_available_message_types()
55
+ display_names = self.settings_manager.get_message_type_display_names()
56
+
57
+ # Create inline keyboard buttons in 2x2 layout
58
+ buttons = []
59
+ row = []
60
+
61
+ for i, msg_type in enumerate(message_types):
62
+ is_shown = msg_type in user_settings.show_message_types
63
+ checkbox = "☑️" if is_shown else "⬜"
64
+ display_name = display_names.get(msg_type, msg_type)
65
+ button = InlineButton(
66
+ text=f"{checkbox} Show {display_name}",
67
+ callback_data=f"toggle_msg_{msg_type}",
68
+ )
69
+ row.append(button)
70
+
71
+ # Create 2x2 layout
72
+ if len(row) == 2 or i == len(message_types) - 1:
73
+ buttons.append(row)
74
+ row = []
75
+
76
+ # Add info button on its own row
77
+ buttons.append(
78
+ [InlineButton("ℹ️ About Message Types", callback_data="info_msg_types")]
79
+ )
80
+
81
+ keyboard = InlineKeyboard(buttons=buttons)
82
+
83
+ # Send settings message with escaped dash
84
+ agent_label = self._get_agent_display_name(context)
85
+ await self.im_client.send_message_with_buttons(
86
+ context,
87
+ f"⚙️ *Settings \\- Message Visibility*\n\nSelect which message types to hide from {agent_label} output:",
88
+ keyboard,
89
+ )
90
+
91
+ async def _handle_settings_slack(self, context: MessageContext):
92
+ """Handle settings for Slack using modal dialog"""
93
+ # For slash commands or direct triggers, we might have trigger_id
94
+ trigger_id = (
95
+ context.platform_specific.get("trigger_id")
96
+ if context.platform_specific
97
+ else None
98
+ )
99
+
100
+ if trigger_id and hasattr(self.im_client, "open_settings_modal"):
101
+ # We have trigger_id, open modal directly
102
+ settings_key = self._get_settings_key(context)
103
+ user_settings = self.settings_manager.get_user_settings(settings_key)
104
+ message_types = self.settings_manager.get_available_message_types()
105
+ display_names = self.settings_manager.get_message_type_display_names()
106
+
107
+ # Get current require_mention override for this channel
108
+ current_require_mention = self.settings_manager.get_require_mention_override(settings_key)
109
+ global_require_mention = self.config.slack.require_mention
110
+
111
+ try:
112
+ await self.im_client.open_settings_modal(
113
+ trigger_id,
114
+ user_settings,
115
+ message_types,
116
+ display_names,
117
+ context.channel_id,
118
+ current_require_mention=current_require_mention,
119
+ global_require_mention=global_require_mention,
120
+ )
121
+ except Exception as e:
122
+ logger.error(f"Error opening settings modal: {e}")
123
+ await self.im_client.send_message(
124
+ context, "❌ Failed to open settings. Please try again."
125
+ )
126
+ else:
127
+ # No trigger_id, show button to open modal
128
+ buttons = [
129
+ [
130
+ InlineButton(
131
+ text="🛠️ Open Settings", callback_data="open_settings_modal"
132
+ )
133
+ ]
134
+ ]
135
+
136
+ keyboard = InlineKeyboard(buttons=buttons)
137
+
138
+ await self.im_client.send_message_with_buttons(
139
+ context,
140
+ f"⚙️ *Personalization Settings*\n\nConfigure how {self._get_agent_display_name(context)} messages appear in your Slack workspace.",
141
+ keyboard,
142
+ )
143
+
144
+ async def handle_toggle_message_type(self, context: MessageContext, msg_type: str):
145
+ """Handle toggle for message type visibility"""
146
+ try:
147
+ # Toggle message type visibility
148
+ settings_key = self._get_settings_key(context)
149
+ is_shown = self.settings_manager.toggle_show_message_type(
150
+ settings_key, msg_type
151
+ )
152
+
153
+ # Update the keyboard
154
+ user_settings = self.settings_manager.get_user_settings(settings_key)
155
+ message_types = self.settings_manager.get_available_message_types()
156
+ display_names = self.settings_manager.get_message_type_display_names()
157
+
158
+ buttons = []
159
+ row = []
160
+
161
+ for i, mt in enumerate(message_types):
162
+ is_shown_now = mt in user_settings.show_message_types
163
+ checkbox = "☑️" if is_shown_now else "⬜"
164
+ display_name = display_names.get(mt, mt)
165
+ button = InlineButton(
166
+ text=f"{checkbox} Show {display_name}",
167
+ callback_data=f"toggle_msg_{mt}",
168
+ )
169
+ row.append(button)
170
+
171
+ # Create 2x2 layout
172
+ if len(row) == 2 or i == len(message_types) - 1:
173
+ buttons.append(row)
174
+ row = []
175
+
176
+ buttons.append(
177
+ [InlineButton("ℹ️ About Message Types", callback_data="info_msg_types")]
178
+ )
179
+
180
+ keyboard = InlineKeyboard(buttons=buttons)
181
+
182
+ # Update message
183
+ if context.message_id:
184
+ await self.im_client.edit_message(
185
+ context, context.message_id, keyboard=keyboard
186
+ )
187
+
188
+ # Answer callback (for Telegram)
189
+ display_name = display_names.get(msg_type, msg_type)
190
+ action = "shown" if is_shown else "hidden"
191
+
192
+ # Platform-specific callback answering
193
+ await self.im_client.send_message(
194
+ context, f"{display_name} messages are now {action}"
195
+ )
196
+
197
+ except Exception as e:
198
+ logger.error(f"Error toggling message type {msg_type}: {e}")
199
+ await self.im_client.send_message(
200
+ context,
201
+ self.formatter.format_error(f"Failed to toggle setting: {str(e)}"),
202
+ )
203
+
204
+ async def handle_info_message_types(self, context: MessageContext):
205
+ """Show information about different message types"""
206
+ try:
207
+ formatter = self.im_client.formatter
208
+
209
+ # Use the new format_info_message method for clean, platform-agnostic formatting
210
+ info_text = formatter.format_info_message(
211
+ title="Message Types Info:",
212
+ emoji="📋",
213
+ items=[
214
+ ("System", "System initialization and status messages"),
215
+ ("Toolcall", "Agent tool name + params (one line)"),
216
+ ("Assistant", "Agent responses and explanations"),
217
+ ("Result", "Final execution result (always sent)"),
218
+ ],
219
+ footer="Hidden messages won't be sent to your IM platform.",
220
+ )
221
+
222
+ # Send as new message
223
+ await self.im_client.send_message(context, info_text)
224
+ logger.info(f"Sent info_msg_types message to user {context.user_id}")
225
+
226
+ except Exception as e:
227
+ logger.error(f"Error in info_msg_types handler: {e}", exc_info=True)
228
+ await self.im_client.send_message(
229
+ context, "❌ Error showing message types info"
230
+ )
231
+
232
+ async def handle_info_how_it_works(self, context: MessageContext):
233
+ """Show information about how the bot works"""
234
+ try:
235
+ formatter = self.im_client.formatter
236
+ agent_label = self._get_agent_display_name(context)
237
+
238
+ # Use format_info_message for clean, platform-agnostic formatting
239
+ info_text = formatter.format_info_message(
240
+ title="How Vibe Remote Works:",
241
+ emoji="📚",
242
+ items=[
243
+ ("Real-time", f"Messages are immediately sent to {agent_label}"),
244
+ ("Persistent", "Each chat maintains its own conversation context"),
245
+ ("Commands", "Use @Vibe Remote /start for menu, @Vibe Remote /clear to reset session"),
246
+ ("Work Dir", "Change working directory with /set_cwd or via menu"),
247
+ ("Settings", "Customize message visibility in Settings"),
248
+ ],
249
+ footer=f"Just type normally to chat with {agent_label}!",
250
+ )
251
+
252
+ # Send as new message
253
+ await self.im_client.send_message(context, info_text)
254
+ logger.info(f"Sent how_it_works info to user {context.user_id}")
255
+
256
+ except Exception as e:
257
+ logger.error(f"Error in handle_info_how_it_works: {e}", exc_info=True)
258
+ await self.im_client.send_message(
259
+ context, "❌ Error showing help information"
260
+ )
261
+
262
+ async def handle_routing(self, context: MessageContext):
263
+ """Handle routing command - show agent/model selection"""
264
+ try:
265
+ # Only Slack has modal support for now
266
+ if self.config.platform == "slack":
267
+ await self._handle_routing_slack(context)
268
+ else:
269
+ # For other platforms, show a simple message
270
+ await self.im_client.send_message(
271
+ context,
272
+ "🤖 Agent switching is currently only available in Slack. "
273
+ "Use Slack Agent Settings to configure routing.",
274
+ )
275
+ except Exception as e:
276
+ logger.error(f"Error showing routing settings: {e}", exc_info=True)
277
+ await self.im_client.send_message(
278
+ context, f"❌ Error showing routing settings: {str(e)}"
279
+ )
280
+
281
+ async def _handle_routing_slack(self, context: MessageContext):
282
+ """Handle routing for Slack using modal dialog"""
283
+ trigger_id = (
284
+ context.platform_specific.get("trigger_id")
285
+ if context.platform_specific
286
+ else None
287
+ )
288
+
289
+ if not trigger_id:
290
+ # No trigger_id, show button to open modal
291
+ buttons = [
292
+ [
293
+ InlineButton(
294
+ text="🤖 Open Agent Settings",
295
+ callback_data="open_routing_modal",
296
+ )
297
+ ]
298
+ ]
299
+ keyboard = InlineKeyboard(buttons=buttons)
300
+ await self.im_client.send_message_with_buttons(
301
+ context,
302
+ "🤖 *Agent & Model Settings*\n\nConfigure which backend to use for this channel.",
303
+ keyboard,
304
+ )
305
+ return
306
+
307
+ # Gather data for the modal
308
+ settings_key = self._get_settings_key(context)
309
+ current_routing = self.settings_manager.get_channel_routing(settings_key)
310
+
311
+ # Get registered backends, prioritize opencode first
312
+ all_backends = list(self.controller.agent_service.agents.keys())
313
+ registered_backends = sorted(
314
+ all_backends, key=lambda x: (x != "opencode", x)
315
+ )
316
+
317
+ # Get current backend (from routing or default)
318
+ current_backend = self.controller.resolve_agent_for_context(context)
319
+
320
+ # Get current require_mention override for this channel
321
+ current_require_mention = self.settings_manager.get_require_mention_override(settings_key)
322
+ global_require_mention = self.config.slack.require_mention
323
+
324
+ # Get OpenCode agents/models if available
325
+ opencode_agents = []
326
+ opencode_models = {}
327
+ opencode_default_config = {}
328
+
329
+ if "opencode" in registered_backends:
330
+ try:
331
+ # Get OpenCode server manager
332
+ opencode_agent = self.controller.agent_service.agents.get("opencode")
333
+ if opencode_agent and hasattr(opencode_agent, "_get_server"):
334
+ server = await opencode_agent._get_server()
335
+ await server.ensure_running()
336
+
337
+ cwd = self.controller.get_cwd(context)
338
+ opencode_agents = await server.get_available_agents(cwd)
339
+ opencode_models = await server.get_available_models(cwd)
340
+ opencode_default_config = await server.get_default_config(cwd)
341
+ except Exception as e:
342
+ logger.warning(f"Failed to fetch OpenCode data: {e}")
343
+
344
+ # Open modal
345
+ try:
346
+ await self.im_client.open_routing_modal(
347
+ trigger_id=trigger_id,
348
+ channel_id=context.channel_id,
349
+ registered_backends=registered_backends,
350
+ current_backend=current_backend,
351
+ current_routing=current_routing,
352
+ opencode_agents=opencode_agents,
353
+ opencode_models=opencode_models,
354
+ opencode_default_config=opencode_default_config,
355
+ current_require_mention=current_require_mention,
356
+ global_require_mention=global_require_mention,
357
+ )
358
+ except Exception as e:
359
+ logger.error(f"Error opening routing modal: {e}", exc_info=True)
360
+ await self.im_client.send_message(
361
+ context, "❌ Failed to open settings. Please try again."
362
+ )
modules/__init__.py ADDED
File without changes
@@ -0,0 +1,58 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from typing import Dict, Optional
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ @dataclass
11
+ class PlatformRoute:
12
+ default: str = "claude"
13
+ overrides: Dict[str, str] = field(default_factory=dict)
14
+
15
+
16
+ class AgentRouter:
17
+ """Resolve which agent should serve a given message context."""
18
+
19
+ def __init__(
20
+ self,
21
+ platform_routes: Dict[str, PlatformRoute],
22
+ global_default: str = "claude",
23
+ ):
24
+ self.platform_routes = platform_routes
25
+ self.global_default = global_default
26
+
27
+ @classmethod
28
+ def from_file(
29
+ cls, file_path: Optional[str], *, platform: str
30
+ ) -> "AgentRouter":
31
+ routes: Dict[str, PlatformRoute] = {}
32
+ global_default = "claude"
33
+
34
+ # File-based routing removed; keep defaults only.
35
+ routes.setdefault(platform, PlatformRoute(default=global_default))
36
+ return cls(routes, global_default=global_default)
37
+
38
+ @staticmethod
39
+ def _load_file(path: str) -> Dict:
40
+ _, ext = os.path.splitext(path)
41
+ if ext.lower() in {".yaml", ".yml"}:
42
+ try:
43
+ import yaml # type: ignore
44
+ except ImportError as exc:
45
+ raise RuntimeError(
46
+ "PyYAML is required to parse YAML agent route files. "
47
+ "Install with `pip install pyyaml` or use JSON."
48
+ ) from exc
49
+ with open(path, "r") as f:
50
+ return yaml.safe_load(f) or {}
51
+ with open(path, "r") as f:
52
+ return json.load(f)
53
+
54
+ def resolve(self, platform: str, channel_id: str) -> str:
55
+ platform_route = self.platform_routes.get(platform)
56
+ if not platform_route:
57
+ return self.global_default
58
+ return platform_route.overrides.get(channel_id, platform_route.default)
@@ -0,0 +1,38 @@
1
+ from typing import Optional
2
+
3
+ from .base import BaseAgent, AgentRequest, AgentMessage
4
+ from .claude_agent import ClaudeAgent
5
+ from .codex_agent import CodexAgent
6
+ from .opencode_agent import OpenCodeAgent
7
+ from .service import AgentService
8
+
9
+
10
+ def get_agent_display_name(agent_name: Optional[str], fallback: Optional[str] = None) -> str:
11
+ normalized_map = {
12
+ "claude": "Claude",
13
+ "codex": "Codex",
14
+ "opencode": "OpenCode",
15
+ }
16
+
17
+ candidate = (agent_name or fallback or "Agent").strip()
18
+ if not candidate:
19
+ candidate = "Agent"
20
+
21
+ normalized = candidate.lower()
22
+ friendly = normalized_map.get(normalized)
23
+ if friendly:
24
+ return friendly
25
+
26
+ return candidate.replace("_", " ").title()
27
+
28
+
29
+ __all__ = [
30
+ "AgentMessage",
31
+ "AgentRequest",
32
+ "BaseAgent",
33
+ "ClaudeAgent",
34
+ "CodexAgent",
35
+ "OpenCodeAgent",
36
+ "AgentService",
37
+ "get_agent_display_name",
38
+ ]
modules/agents/base.py ADDED
@@ -0,0 +1,91 @@
1
+ """Abstract agent interfaces and shared dataclasses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, Optional
9
+
10
+ from modules.im import MessageContext
11
+
12
+
13
+ @dataclass
14
+ class AgentRequest:
15
+ """Normalized agent invocation request."""
16
+
17
+ context: MessageContext
18
+ message: str
19
+ working_path: str
20
+ base_session_id: str
21
+ composite_session_id: str
22
+ settings_key: str
23
+ ack_message_id: Optional[str] = None
24
+ subagent_name: Optional[str] = None
25
+ subagent_key: Optional[str] = None
26
+ subagent_model: Optional[str] = None
27
+ subagent_reasoning_effort: Optional[str] = None
28
+ last_agent_message: Optional[str] = None
29
+ last_agent_message_parse_mode: Optional[str] = None
30
+ started_at: float = field(default_factory=time.monotonic)
31
+
32
+
33
+ @dataclass
34
+ class AgentMessage:
35
+ """Normalized message emitted by an agent implementation."""
36
+
37
+ text: str
38
+ message_type: str = "assistant"
39
+ parse_mode: str = "markdown"
40
+ metadata: Optional[Dict[str, Any]] = None
41
+
42
+
43
+ class BaseAgent(ABC):
44
+ """Abstract base class for all agent implementations."""
45
+
46
+ name: str
47
+
48
+ def __init__(self, controller):
49
+ self.controller = controller
50
+ self.config = controller.config
51
+ self.im_client = controller.im_client
52
+ self.settings_manager = controller.settings_manager
53
+
54
+ def _calculate_duration_ms(self, started_at: Optional[float]) -> int:
55
+ if not started_at:
56
+ return 0
57
+ elapsed = time.monotonic() - started_at
58
+ return max(0, int(elapsed * 1000))
59
+
60
+ async def emit_result_message(
61
+ self,
62
+ context: MessageContext,
63
+ result_text: Optional[str],
64
+ subtype: str = "success",
65
+ duration_ms: Optional[int] = None,
66
+ started_at: Optional[float] = None,
67
+ parse_mode: str = "markdown",
68
+ suffix: Optional[str] = None,
69
+ ) -> None:
70
+ if duration_ms is None:
71
+ duration_ms = self._calculate_duration_ms(started_at)
72
+ formatted = self.im_client.formatter.format_result_message(
73
+ subtype or "", duration_ms, result_text
74
+ )
75
+ if suffix:
76
+ formatted = f"{formatted}\n{suffix}"
77
+ await self.controller.emit_agent_message(
78
+ context, "result", formatted, parse_mode=parse_mode
79
+ )
80
+
81
+ @abstractmethod
82
+ async def handle_message(self, request: AgentRequest) -> None:
83
+ """Process a user message routed to this agent."""
84
+
85
+ async def clear_sessions(self, settings_key: str) -> int:
86
+ """Clear session state for a given settings key. Returns cleared count."""
87
+ return 0
88
+
89
+ async def handle_stop(self, request: AgentRequest) -> bool:
90
+ """Attempt to interrupt an in-flight task. Returns True if handled."""
91
+ return False