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,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)
|
modules/claude_client.py
ADDED
|
@@ -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
|
+
|