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,41 @@
1
+ import logging
2
+ from typing import Dict, Optional
3
+
4
+ from .base import AgentRequest, BaseAgent
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class AgentService:
10
+ """Registry and dispatcher for agent implementations."""
11
+
12
+ def __init__(self, controller):
13
+ self.controller = controller
14
+ self.agents: Dict[str, BaseAgent] = {}
15
+ self.default_agent = "claude"
16
+
17
+ def register(self, agent: BaseAgent):
18
+ self.agents[agent.name] = agent
19
+ logger.info(f"Registered agent backend: {agent.name}")
20
+
21
+ def get(self, agent_name: Optional[str]) -> BaseAgent:
22
+ target = agent_name or self.default_agent
23
+ if target in self.agents:
24
+ return self.agents[target]
25
+ raise KeyError(target)
26
+
27
+ async def handle_message(self, agent_name: str, request: AgentRequest):
28
+ agent = self.get(agent_name)
29
+ await agent.handle_message(request)
30
+
31
+ async def clear_sessions(self, settings_key: str) -> Dict[str, int]:
32
+ cleared: Dict[str, int] = {}
33
+ for name, agent in self.agents.items():
34
+ count = await agent.clear_sessions(settings_key)
35
+ if count:
36
+ cleared[name] = count
37
+ return cleared
38
+
39
+ async def handle_stop(self, agent_name: str, request: AgentRequest) -> bool:
40
+ agent = self.get(agent_name)
41
+ return await agent.handle_stop(request)
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Dict, Iterable, Optional
8
+
9
+ import yaml
10
+
11
+
12
+ _PREFIX_PATTERN = re.compile(r"^([^\s::]+)\s*[::]\s*(.*)$", re.DOTALL)
13
+
14
+
15
+ def _yaml_safe_load(text: str) -> dict:
16
+ data = yaml.safe_load(text)
17
+ return data if isinstance(data, dict) else {}
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class SubagentDefinition:
22
+ name: str
23
+ model: Optional[str] = None
24
+ reasoning_effort: Optional[str] = None
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class PrefixMatch:
29
+ name: str
30
+ message: str
31
+
32
+
33
+ def parse_subagent_prefix(message: str) -> Optional[PrefixMatch]:
34
+ if not message:
35
+ return None
36
+
37
+ trimmed = message.lstrip()
38
+ match = _PREFIX_PATTERN.match(trimmed)
39
+ if not match:
40
+ return None
41
+
42
+ name = match.group(1).strip()
43
+ body = match.group(2)
44
+ if not name:
45
+ return None
46
+
47
+ if not body or not body.strip():
48
+ return None
49
+
50
+ return PrefixMatch(name=name, message=body.strip())
51
+
52
+
53
+ def normalize_subagent_name(name: str) -> str:
54
+ return (name or "").strip().lower()
55
+
56
+
57
+ def list_claude_subagents(search_root: Optional[Path] = None) -> Dict[str, SubagentDefinition]:
58
+ root = search_root or (Path.home() / ".claude")
59
+ if not root.exists():
60
+ return {}
61
+
62
+ agent_files: list[Path] = []
63
+ candidate_dirs = _find_claude_agent_dirs(root)
64
+ for directory in candidate_dirs:
65
+ agent_files.extend(directory.glob("*.md"))
66
+
67
+ definitions: Dict[str, SubagentDefinition] = {}
68
+ for agent_file in agent_files:
69
+ definition = _parse_claude_agent_definition(agent_file)
70
+ if not definition:
71
+ continue
72
+ key = normalize_subagent_name(definition.name)
73
+ if key:
74
+ definitions[key] = definition
75
+ return definitions
76
+
77
+
78
+ def load_claude_subagent(name: str, search_root: Optional[Path] = None) -> Optional[SubagentDefinition]:
79
+ normalized = normalize_subagent_name(name)
80
+ if not normalized:
81
+ return None
82
+ return list_claude_subagents(search_root).get(normalized)
83
+
84
+
85
+ def _find_claude_agent_dirs(root: Path) -> Iterable[Path]:
86
+ agent_dirs = []
87
+ if (root / "agents").exists():
88
+ agent_dirs.append(root / "agents")
89
+
90
+ for path in root.rglob("agents"):
91
+ if path.is_dir():
92
+ agent_dirs.append(path)
93
+
94
+ seen = set()
95
+ unique = []
96
+ for path in agent_dirs:
97
+ resolved = os.path.normpath(str(path))
98
+ if resolved in seen:
99
+ continue
100
+ seen.add(resolved)
101
+ unique.append(path)
102
+ return unique
103
+
104
+
105
+ def _parse_claude_agent_definition(path: Path) -> Optional[SubagentDefinition]:
106
+ try:
107
+ text = path.read_text(encoding="utf-8")
108
+ except Exception:
109
+ return None
110
+
111
+ if not text.startswith("---"):
112
+ return None
113
+
114
+ parts = text.split("---", 2)
115
+ if len(parts) < 3:
116
+ return None
117
+
118
+ header = parts[1]
119
+ try:
120
+ data = _yaml_safe_load(header)
121
+ except Exception:
122
+ return None
123
+
124
+ name = data.get("name")
125
+ if not isinstance(name, str) or not name.strip():
126
+ return None
127
+
128
+ model = data.get("model")
129
+ if not isinstance(model, str) or not model.strip():
130
+ model = None
131
+
132
+ reasoning_effort = data.get("reasoning_effort") or data.get("reasoningEffort")
133
+ if not isinstance(reasoning_effort, str) or not reasoning_effort.strip():
134
+ reasoning_effort = None
135
+
136
+ return SubagentDefinition(name=name.strip(), model=model, reasoning_effort=reasoning_effort)
@@ -0,0 +1,154 @@
1
+ import logging
2
+ import os
3
+ from typing import Optional, Callable
4
+ from claude_code_sdk import (
5
+ ClaudeCodeOptions,
6
+ SystemMessage,
7
+ AssistantMessage,
8
+ UserMessage,
9
+ ResultMessage,
10
+ TextBlock,
11
+ ToolUseBlock,
12
+ ToolResultBlock,
13
+ )
14
+ from config.v2_compat import ClaudeCompatConfig
15
+ from modules.im.formatters import BaseMarkdownFormatter, SlackFormatter
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class ClaudeClient:
22
+ def __init__(
23
+ self, config: ClaudeCompatConfig, formatter: Optional[BaseMarkdownFormatter] = None
24
+ ):
25
+ self.config = config
26
+ self.formatter = formatter or SlackFormatter()
27
+ self.options = ClaudeCodeOptions(
28
+ permission_mode=config.permission_mode, # type: ignore[arg-type]
29
+ cwd=config.cwd,
30
+ system_prompt=config.system_prompt,
31
+ ) # type: ignore[arg-type]
32
+
33
+ def format_message(
34
+ self, message, get_relative_path: Optional[Callable[[str], str]] = None
35
+ ) -> str:
36
+ """Format different types of messages according to specified rules"""
37
+ try:
38
+ if isinstance(message, SystemMessage):
39
+ return self._format_system_message(message)
40
+ elif isinstance(message, AssistantMessage):
41
+ return self._format_assistant_message(message, get_relative_path)
42
+ elif isinstance(message, UserMessage):
43
+ return self._format_user_message(message, get_relative_path)
44
+ elif isinstance(message, ResultMessage):
45
+ return self._format_result_message(message)
46
+ else:
47
+ return self.formatter.format_warning(
48
+ f"Unknown message type: {type(message)}"
49
+ )
50
+ except Exception as e:
51
+ logger.error(f"Error formatting message: {e}")
52
+ return self.formatter.format_error(f"Error formatting message: {str(e)}")
53
+
54
+ def _process_content_blocks(
55
+ self, content_blocks, get_relative_path: Optional[Callable[[str], str]] = None
56
+ ) -> list:
57
+ """Process content blocks (TextBlock, ToolUseBlock) and return formatted parts"""
58
+ formatted_parts = []
59
+
60
+ for block in content_blocks:
61
+ if isinstance(block, TextBlock):
62
+ # Don't escape here - let the formatter handle it during final formatting
63
+ # This avoids double escaping
64
+ formatted_parts.append(block.text)
65
+ elif isinstance(block, ToolUseBlock):
66
+ tool_info = self._format_tool_use_block(block, get_relative_path)
67
+ formatted_parts.append(tool_info)
68
+ elif isinstance(block, ToolResultBlock):
69
+ result_info = self._format_tool_result_block(block)
70
+ formatted_parts.append(result_info)
71
+
72
+ return formatted_parts
73
+
74
+ def _get_relative_path(self, full_path: str) -> str:
75
+ """Convert absolute path to relative path based on ClaudeCode cwd"""
76
+ # Get ClaudeCode's current working directory
77
+ cwd = self.options.cwd or os.getcwd()
78
+
79
+ # Normalize paths for consistent comparison
80
+ cwd = os.path.normpath(cwd)
81
+ full_path = os.path.normpath(full_path)
82
+
83
+ try:
84
+ # If the path starts with cwd, make it relative
85
+ if full_path.startswith(cwd + os.sep) or full_path == cwd:
86
+ relative = os.path.relpath(full_path, cwd)
87
+ # Use "./" prefix for current directory files
88
+ if not relative.startswith(".") and relative != ".":
89
+ relative = "./" + relative
90
+ return relative
91
+ else:
92
+ # If not under cwd, just return the path as is
93
+ return full_path
94
+ except:
95
+ # Fallback to original path if any error
96
+ return full_path
97
+
98
+ def _format_tool_use_block(
99
+ self,
100
+ block: ToolUseBlock,
101
+ get_relative_path: Optional[Callable[[str], str]] = None,
102
+ ) -> str:
103
+ """Format ToolUseBlock using formatter"""
104
+ # Prefer caller-provided get_relative_path (per-session cwd), fallback to self
105
+ rel = get_relative_path if get_relative_path else self._get_relative_path
106
+ return self.formatter.format_tool_use(
107
+ block.name, block.input, get_relative_path=rel
108
+ )
109
+
110
+ def _format_tool_result_block(self, block: ToolResultBlock) -> str:
111
+ """Format ToolResultBlock using formatter"""
112
+ is_error = bool(block.is_error) if block.is_error is not None else False
113
+ content = block.content if isinstance(block.content, str) else None
114
+ return self.formatter.format_tool_result(is_error, content)
115
+
116
+ def _format_system_message(self, message: SystemMessage) -> str:
117
+ """Format SystemMessage using formatter"""
118
+ cwd = message.data.get("cwd", "Unknown")
119
+ session_id = message.data.get("session_id", None)
120
+ return self.formatter.format_system_message(cwd, message.subtype, session_id)
121
+
122
+ def _format_assistant_message(
123
+ self,
124
+ message: AssistantMessage,
125
+ get_relative_path: Optional[Callable[[str], str]] = None,
126
+ ) -> str:
127
+ """Format AssistantMessage using formatter"""
128
+ content_parts = self._process_content_blocks(message.content, get_relative_path)
129
+ return self.formatter.format_assistant_message(content_parts)
130
+
131
+ def _format_user_message(
132
+ self,
133
+ message: UserMessage,
134
+ get_relative_path: Optional[Callable[[str], str]] = None,
135
+ ) -> str:
136
+ """Format UserMessage using formatter"""
137
+ content_parts = self._process_content_blocks(message.content, get_relative_path)
138
+ return self.formatter.format_user_message(content_parts)
139
+
140
+ def _format_result_message(self, message: ResultMessage) -> str:
141
+ """Format ResultMessage using formatter"""
142
+ return self.formatter.format_result_message(
143
+ message.subtype, message.duration_ms, message.result
144
+ )
145
+
146
+ def _is_skip_message(self, message) -> bool:
147
+ """Check if the message should be skipped"""
148
+ if isinstance(message, AssistantMessage):
149
+ if not message.content:
150
+ return True
151
+ elif isinstance(message, UserMessage):
152
+ if not message.content:
153
+ return True
154
+ return False
modules/im/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """IM platform abstraction package
2
+
3
+ Provides unified interface for different instant messaging platforms.
4
+
5
+ Example usage:
6
+ from modules.im import BaseIMClient, IMFactory, MessageContext
7
+
8
+ # Create client via factory
9
+ client = IMFactory.create_client(config)
10
+
11
+ # Use platform-agnostic messaging
12
+ context = MessageContext(user_id="123", channel_id="456")
13
+ await client.send_message(context, "Hello!")
14
+ """
15
+
16
+ # Core abstractions
17
+ from .base import (
18
+ BaseIMClient,
19
+ BaseIMConfig,
20
+ MessageContext,
21
+ InlineButton,
22
+ InlineKeyboard,
23
+ )
24
+
25
+ # Factory for client creation
26
+ from .factory import IMFactory
27
+
28
+ # Platform implementations are available but not imported by default
29
+ # to avoid circular import issues. Import them explicitly if needed:
30
+ # from .slack import SlackBot
31
+
32
+ __all__ = [
33
+ "BaseIMClient",
34
+ "BaseIMConfig",
35
+ "MessageContext",
36
+ "InlineButton",
37
+ "InlineKeyboard",
38
+ "IMFactory",
39
+ ]
40
+
41
+ # Convenience function for quick client creation
42
+ def create_client(config):
43
+ """Convenience function to create IM client
44
+
45
+ Args:
46
+ config: Application configuration
47
+
48
+ Returns:
49
+ Platform-specific IM client instance
50
+ """
51
+ return IMFactory.create_client(config)
52
+
53
+
54
+ # Platform information
55
+ def get_supported_platforms():
56
+ """Get list of supported IM platforms
57
+
58
+ Returns:
59
+ List of supported platform names
60
+ """
61
+ return IMFactory.get_supported_platforms()
62
+
63
+