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,13 @@
1
+ """Handler modules for organizing controller functionality"""
2
+
3
+ from .command_handlers import CommandHandlers
4
+ from .session_handler import SessionHandler
5
+ from .settings_handler import SettingsHandler
6
+ from .message_handler import MessageHandler
7
+
8
+ __all__ = [
9
+ 'CommandHandlers',
10
+ 'SessionHandler',
11
+ 'SettingsHandler',
12
+ 'MessageHandler'
13
+ ]
@@ -0,0 +1,342 @@
1
+ """Command handlers for bot commands like /start, /clear, /cwd, etc."""
2
+
3
+ import os
4
+ import logging
5
+ from typing import Optional
6
+ from modules.agents import AgentRequest, get_agent_display_name
7
+ from modules.im import MessageContext, InlineKeyboard, InlineButton
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class CommandHandlers:
13
+ """Handles all bot command operations"""
14
+
15
+ def __init__(self, controller):
16
+ """Initialize with reference to main controller"""
17
+ self.controller = controller
18
+ self.config = controller.config
19
+ self.im_client = controller.im_client
20
+ self.session_manager = controller.session_manager
21
+ self.settings_manager = controller.settings_manager
22
+
23
+ def _get_channel_context(self, context: MessageContext) -> MessageContext:
24
+ """Get context for channel messages (no thread)"""
25
+ # For Slack: send command responses directly to channel, not in thread
26
+ if self.config.platform == "slack":
27
+ return MessageContext(
28
+ user_id=context.user_id,
29
+ channel_id=context.channel_id,
30
+ thread_id=None, # No thread for command responses
31
+ platform_specific=context.platform_specific,
32
+ )
33
+ # For other platforms, keep original context
34
+ return context
35
+
36
+ async def handle_start(self, context: MessageContext, args: str = ""):
37
+ """Handle /start command with interactive buttons"""
38
+ platform_name = self.config.platform.capitalize()
39
+
40
+ # Get user and channel info
41
+ try:
42
+ user_info = await self.im_client.get_user_info(context.user_id)
43
+ except Exception as e:
44
+ logger.warning(f"Failed to get user info: {e}")
45
+ user_info = {"id": context.user_id}
46
+
47
+ try:
48
+ channel_info = await self.im_client.get_channel_info(context.channel_id)
49
+ except Exception as e:
50
+ logger.warning(f"Failed to get channel info: {e}")
51
+ channel_info = {
52
+ "id": context.channel_id,
53
+ "name": (
54
+ "Direct Message"
55
+ if context.channel_id.startswith("D")
56
+ else context.channel_id
57
+ ),
58
+ }
59
+
60
+ agent_name = self.controller.resolve_agent_for_context(context)
61
+ default_agent = getattr(self.controller.agent_service, "default_agent", None)
62
+ agent_display_name = get_agent_display_name(
63
+ agent_name, fallback=default_agent or "Unknown"
64
+ )
65
+
66
+ # For non-Slack platforms, use traditional text message
67
+ if self.config.platform != "slack":
68
+ formatter = self.im_client.formatter
69
+
70
+ # Build welcome message using formatter to handle escaping properly
71
+ lines = [
72
+ formatter.format_bold("Welcome to Vibe Remote!"),
73
+ "",
74
+ f"Platform: {formatter.format_text(platform_name)}",
75
+ f"Agent: {formatter.format_text(agent_display_name)}",
76
+ f"User ID: {formatter.format_code_inline(context.user_id)}",
77
+ f"Channel/Chat ID: {formatter.format_code_inline(context.channel_id)}",
78
+ "",
79
+ formatter.format_bold("Commands:"),
80
+ formatter.format_text("@Vibe Remote /start - Show this message"),
81
+ formatter.format_text("@Vibe Remote /clear - Reset session and start fresh"),
82
+ formatter.format_text("@Vibe Remote /cwd - Show current working directory"),
83
+ formatter.format_text("@Vibe Remote /set_cwd <path> - Set working directory"),
84
+ formatter.format_text("@Vibe Remote /settings - Personalization settings"),
85
+ formatter.format_text(
86
+ f"@Vibe Remote /stop - Interrupt {agent_display_name} execution"
87
+ ),
88
+ "",
89
+ formatter.format_bold("How it works:"),
90
+ formatter.format_text(
91
+ f"â€ĸ Send any message and it's immediately sent to {agent_display_name}"
92
+ ),
93
+ formatter.format_text(
94
+ "â€ĸ Each chat maintains its own conversation context"
95
+ ),
96
+ formatter.format_text("â€ĸ Use /clear to reset the conversation"),
97
+ ]
98
+
99
+ message_text = formatter.format_message(*lines)
100
+ channel_context = self._get_channel_context(context)
101
+ await self.im_client.send_message(channel_context, message_text)
102
+ return
103
+
104
+ # For Slack, create interactive buttons using Block Kit
105
+ user_name = user_info.get("real_name") or user_info.get("name") or "User"
106
+
107
+ # Create interactive buttons for commands
108
+ buttons = [
109
+ # Row 1: Directory management
110
+ [
111
+ InlineButton(text="📁 Current Dir", callback_data="cmd_cwd"),
112
+ InlineButton(text="📂 Change Work Dir", callback_data="cmd_change_cwd"),
113
+ ],
114
+ # Row 2: Session and Settings
115
+ [
116
+ InlineButton(text="🔄 Clear All Session", callback_data="cmd_clear"),
117
+ InlineButton(text="âš™ī¸ Settings", callback_data="cmd_settings"),
118
+ ],
119
+ # Row 3: Agent/Model switching
120
+ [InlineButton(text="🤖 Agent Settings", callback_data="cmd_routing")],
121
+ # Row 4: Help
122
+ [InlineButton(text="â„šī¸ How it Works", callback_data="info_how_it_works")],
123
+ ]
124
+
125
+ keyboard = InlineKeyboard(buttons=buttons)
126
+
127
+ welcome_text = f"""🎉 **Welcome to Vibe Remote!**
128
+
129
+ 👋 Hello **{user_name}**!
130
+ 🔧 Platform: **{platform_name}**
131
+ 🤖 Agent: **{agent_display_name}**
132
+ 📍 Channel: **{channel_info.get('name', 'Unknown')}**
133
+
134
+ **Quick Actions:**
135
+ Use the buttons below to manage your {agent_display_name} sessions, or simply type any message to start chatting with {agent_display_name}!"""
136
+
137
+ # Send command response to channel (not in thread)
138
+ channel_context = self._get_channel_context(context)
139
+ await self.im_client.send_message_with_buttons(
140
+ channel_context, welcome_text, keyboard
141
+ )
142
+
143
+ async def handle_clear(self, context: MessageContext, args: str = ""):
144
+ """Handle clear command - clears all sessions across configured agents"""
145
+ try:
146
+ # Get the correct settings key (channel_id for Slack, not user_id)
147
+ settings_key = self.controller._get_settings_key(context)
148
+
149
+ cleared = await self.controller.agent_service.clear_sessions(settings_key)
150
+ if not cleared:
151
+ full_response = (
152
+ "📋 No active sessions to clear.\n🔄 Session state has been reset."
153
+ )
154
+ else:
155
+ details = "\n".join(
156
+ f"â€ĸ {agent} → {count} session(s)" for agent, count in cleared.items()
157
+ )
158
+ full_response = (
159
+ "✅ Cleared active sessions for:\n" f"{details}\n🔄 All sessions reset."
160
+ )
161
+
162
+ channel_context = self._get_channel_context(context)
163
+ await self.im_client.send_message(channel_context, full_response)
164
+ logger.info(f"Sent clear response to user {context.user_id}")
165
+
166
+ except Exception as e:
167
+ logger.error(f"Error clearing session: {e}", exc_info=True)
168
+ try:
169
+ channel_context = self._get_channel_context(context)
170
+ await self.im_client.send_message(
171
+ channel_context, f"❌ Error clearing session: {str(e)}"
172
+ )
173
+ except Exception as send_error:
174
+ logger.error(
175
+ f"Failed to send error message: {send_error}", exc_info=True
176
+ )
177
+
178
+ async def handle_cwd(self, context: MessageContext, args: str = ""):
179
+ """Handle cwd command - show current working directory"""
180
+ try:
181
+ # Get CWD based on context (channel/chat)
182
+ absolute_path = self.controller.get_cwd(context)
183
+
184
+ # Build response using formatter to avoid escaping issues
185
+ formatter = self.im_client.formatter
186
+
187
+ # Format path properly with code block
188
+ path_line = f"📁 Current Working Directory:\n{formatter.format_code_inline(absolute_path)}"
189
+
190
+ # Build status lines
191
+ status_lines = []
192
+ if os.path.exists(absolute_path):
193
+ status_lines.append("✅ Directory exists")
194
+ else:
195
+ status_lines.append("âš ī¸ Directory does not exist")
196
+
197
+ status_lines.append("💡 This is where Agent will execute commands")
198
+
199
+ # Combine all parts
200
+ response_text = path_line + "\n" + "\n".join(status_lines)
201
+
202
+ channel_context = self._get_channel_context(context)
203
+ await self.im_client.send_message(channel_context, response_text)
204
+ except Exception as e:
205
+ logger.error(f"Error getting cwd: {e}")
206
+ channel_context = self._get_channel_context(context)
207
+ await self.im_client.send_message(
208
+ channel_context, f"Error getting working directory: {str(e)}"
209
+ )
210
+
211
+ async def handle_set_cwd(self, context: MessageContext, args: str):
212
+ """Handle set_cwd command - change working directory"""
213
+ try:
214
+ if not args:
215
+ channel_context = self._get_channel_context(context)
216
+ await self.im_client.send_message(
217
+ channel_context, "Usage: /set_cwd <path>"
218
+ )
219
+ return
220
+
221
+ new_path = args.strip()
222
+
223
+ # Expand user path and get absolute path
224
+ expanded_path = os.path.expanduser(new_path)
225
+ absolute_path = os.path.abspath(expanded_path)
226
+
227
+ # Check if directory exists
228
+ if not os.path.exists(absolute_path):
229
+ # Try to create it
230
+ try:
231
+ os.makedirs(absolute_path, exist_ok=True)
232
+ logger.info(f"Created directory: {absolute_path}")
233
+ except Exception as e:
234
+ channel_context = self._get_channel_context(context)
235
+ await self.im_client.send_message(
236
+ channel_context, f"❌ Cannot create directory: {str(e)}"
237
+ )
238
+ return
239
+
240
+ if not os.path.isdir(absolute_path):
241
+ formatter = self.im_client.formatter
242
+ error_text = f"❌ Path exists but is not a directory: {formatter.format_code_inline(absolute_path)}"
243
+ channel_context = self._get_channel_context(context)
244
+ await self.im_client.send_message(channel_context, error_text)
245
+ return
246
+
247
+ # Save to user settings
248
+ settings_key = self.controller._get_settings_key(context)
249
+ self.settings_manager.set_custom_cwd(settings_key, absolute_path)
250
+
251
+ logger.info(f"User {context.user_id} changed cwd to: {absolute_path}")
252
+
253
+ formatter = self.im_client.formatter
254
+ response_text = (
255
+ f"✅ Working directory changed to:\n"
256
+ f"{formatter.format_code_inline(absolute_path)}"
257
+ )
258
+ channel_context = self._get_channel_context(context)
259
+ await self.im_client.send_message(channel_context, response_text)
260
+
261
+ except Exception as e:
262
+ logger.error(f"Error setting cwd: {e}")
263
+ channel_context = self._get_channel_context(context)
264
+ await self.im_client.send_message(
265
+ channel_context, f"❌ Error setting working directory: {str(e)}"
266
+ )
267
+
268
+ async def handle_change_cwd_modal(self, context: MessageContext):
269
+ """Handle Change Work Dir button - open modal for Slack"""
270
+ if self.config.platform != "slack":
271
+ # For non-Slack platforms, just send instructions
272
+ channel_context = self._get_channel_context(context)
273
+ await self.im_client.send_message(
274
+ channel_context,
275
+ "📂 To change working directory, use:\n`/set_cwd <path>`\n\nExample:\n`/set_cwd ~/projects/myapp`",
276
+ )
277
+ return
278
+
279
+ # For Slack, open a modal dialog
280
+ trigger_id = (
281
+ context.platform_specific.get("trigger_id")
282
+ if context.platform_specific
283
+ else None
284
+ )
285
+
286
+ if trigger_id and hasattr(self.im_client, "open_change_cwd_modal"):
287
+ try:
288
+ # Get current CWD based on context
289
+ current_cwd = self.controller.get_cwd(context)
290
+
291
+ await self.im_client.open_change_cwd_modal(
292
+ trigger_id, current_cwd, context.channel_id
293
+ )
294
+ except Exception as e:
295
+ logger.error(f"Error opening change CWD modal: {e}")
296
+ channel_context = self._get_channel_context(context)
297
+ await self.im_client.send_message(
298
+ channel_context,
299
+ "❌ Failed to open directory change dialog. Please try again.",
300
+ )
301
+ else:
302
+ # No trigger_id, show instructions
303
+ channel_context = self._get_channel_context(context)
304
+ await self.im_client.send_message(
305
+ channel_context,
306
+ "📂 Click the 'Change Work Dir' button in the @Vibe Remote /start menu to change working directory.",
307
+ )
308
+
309
+ async def handle_stop(self, context: MessageContext, args: str = ""):
310
+ """Handle /stop command - send interrupt message to the active agent"""
311
+ try:
312
+ session_handler = self.controller.session_handler
313
+ base_session_id, working_path, composite_key = (
314
+ session_handler.get_session_info(context)
315
+ )
316
+ settings_key = self.controller._get_settings_key(context)
317
+ agent_name = self.controller.resolve_agent_for_context(context)
318
+ request = AgentRequest(
319
+ context=context,
320
+ message="stop",
321
+ working_path=working_path,
322
+ base_session_id=base_session_id,
323
+ composite_session_id=composite_key,
324
+ settings_key=settings_key,
325
+ )
326
+
327
+ handled = await self.controller.agent_service.handle_stop(
328
+ agent_name, request
329
+ )
330
+ if not handled:
331
+ channel_context = self._get_channel_context(context)
332
+ await self.im_client.send_message(
333
+ channel_context, "â„šī¸ No active session to stop for this channel."
334
+ )
335
+
336
+ except Exception as e:
337
+ logger.error(f"Error sending stop command: {e}", exc_info=True)
338
+ # For errors, still use original context to maintain thread consistency
339
+ await self.im_client.send_message(
340
+ context, # Use original context
341
+ f"❌ Error sending stop command: {str(e)}",
342
+ )