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
config/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .paths import (
|
|
2
|
+
get_vibe_remote_dir,
|
|
3
|
+
get_config_dir,
|
|
4
|
+
get_state_dir,
|
|
5
|
+
get_logs_dir,
|
|
6
|
+
get_runtime_dir,
|
|
7
|
+
get_runtime_pid_path,
|
|
8
|
+
get_runtime_ui_pid_path,
|
|
9
|
+
get_runtime_status_path,
|
|
10
|
+
get_runtime_doctor_path,
|
|
11
|
+
get_config_path,
|
|
12
|
+
get_settings_path,
|
|
13
|
+
get_sessions_path,
|
|
14
|
+
ensure_data_dirs,
|
|
15
|
+
)
|
|
16
|
+
from .v2_config import V2Config
|
|
17
|
+
from .v2_settings import SettingsStore
|
|
18
|
+
from .v2_sessions import SessionsStore
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"V2Config",
|
|
22
|
+
"SettingsStore",
|
|
23
|
+
"SessionsStore",
|
|
24
|
+
"get_vibe_remote_dir",
|
|
25
|
+
"get_config_dir",
|
|
26
|
+
"get_state_dir",
|
|
27
|
+
"get_logs_dir",
|
|
28
|
+
"get_runtime_dir",
|
|
29
|
+
"get_runtime_pid_path",
|
|
30
|
+
"get_runtime_ui_pid_path",
|
|
31
|
+
"get_runtime_status_path",
|
|
32
|
+
"get_runtime_doctor_path",
|
|
33
|
+
"get_config_path",
|
|
34
|
+
"get_settings_path",
|
|
35
|
+
"get_sessions_path",
|
|
36
|
+
"ensure_data_dirs",
|
|
37
|
+
]
|
config/paths.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_vibe_remote_dir() -> Path:
|
|
5
|
+
return Path.home() / ".vibe_remote"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_config_dir() -> Path:
|
|
9
|
+
return get_vibe_remote_dir() / "config"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_state_dir() -> Path:
|
|
13
|
+
return get_vibe_remote_dir() / "state"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_logs_dir() -> Path:
|
|
17
|
+
return get_vibe_remote_dir() / "logs"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_runtime_dir() -> Path:
|
|
21
|
+
return get_vibe_remote_dir() / "runtime"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_runtime_pid_path() -> Path:
|
|
25
|
+
return get_runtime_dir() / "vibe.pid"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_runtime_ui_pid_path() -> Path:
|
|
29
|
+
return get_runtime_dir() / "vibe-ui.pid"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_runtime_status_path() -> Path:
|
|
33
|
+
return get_runtime_dir() / "status.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_runtime_doctor_path() -> Path:
|
|
37
|
+
return get_runtime_dir() / "doctor.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_config_path() -> Path:
|
|
41
|
+
return get_config_dir() / "config.json"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_settings_path() -> Path:
|
|
45
|
+
return get_state_dir() / "settings.json"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_sessions_path() -> Path:
|
|
49
|
+
return get_state_dir() / "sessions.json"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def ensure_data_dirs() -> None:
|
|
53
|
+
get_config_dir().mkdir(parents=True, exist_ok=True)
|
|
54
|
+
get_state_dir().mkdir(parents=True, exist_ok=True)
|
|
55
|
+
get_logs_dir().mkdir(parents=True, exist_ok=True)
|
|
56
|
+
get_runtime_dir().mkdir(parents=True, exist_ok=True)
|
config/v2_compat.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from config.v2_config import V2Config, SlackConfig
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ClaudeCompatConfig:
|
|
9
|
+
permission_mode: str
|
|
10
|
+
cwd: str
|
|
11
|
+
system_prompt: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
def __post_init__(self) -> None:
|
|
14
|
+
self.permission_mode = str(self.permission_mode)
|
|
15
|
+
self.cwd = str(self.cwd)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CodexCompatConfig:
|
|
20
|
+
binary: str
|
|
21
|
+
extra_args: list[str]
|
|
22
|
+
default_model: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class OpenCodeCompatConfig:
|
|
27
|
+
binary: str
|
|
28
|
+
port: int
|
|
29
|
+
request_timeout_seconds: int
|
|
30
|
+
error_retry_limit: int = 1 # Max retries on LLM stream errors (0 = no retry)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AppCompatConfig:
|
|
35
|
+
platform: str
|
|
36
|
+
slack: SlackConfig
|
|
37
|
+
claude: ClaudeCompatConfig
|
|
38
|
+
codex: Optional[CodexCompatConfig]
|
|
39
|
+
opencode: Optional[OpenCodeCompatConfig]
|
|
40
|
+
log_level: str
|
|
41
|
+
ack_mode: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def to_app_config(v2: V2Config) -> AppCompatConfig:
|
|
45
|
+
claude = ClaudeCompatConfig(
|
|
46
|
+
permission_mode="default",
|
|
47
|
+
cwd=v2.runtime.default_cwd,
|
|
48
|
+
system_prompt=None,
|
|
49
|
+
)
|
|
50
|
+
codex = None
|
|
51
|
+
if v2.agents.codex.enabled:
|
|
52
|
+
codex = CodexCompatConfig(
|
|
53
|
+
binary=v2.agents.codex.cli_path,
|
|
54
|
+
extra_args=[],
|
|
55
|
+
default_model=v2.agents.codex.default_model,
|
|
56
|
+
)
|
|
57
|
+
opencode = None
|
|
58
|
+
if v2.agents.opencode.enabled:
|
|
59
|
+
opencode = OpenCodeCompatConfig(
|
|
60
|
+
binary=v2.agents.opencode.cli_path,
|
|
61
|
+
port=4096,
|
|
62
|
+
request_timeout_seconds=60,
|
|
63
|
+
error_retry_limit=v2.agents.opencode.error_retry_limit,
|
|
64
|
+
)
|
|
65
|
+
slack = SlackConfig(**v2.slack.__dict__)
|
|
66
|
+
return AppCompatConfig(
|
|
67
|
+
platform="slack",
|
|
68
|
+
slack=slack,
|
|
69
|
+
claude=claude,
|
|
70
|
+
codex=codex,
|
|
71
|
+
opencode=opencode,
|
|
72
|
+
log_level=v2.runtime.log_level,
|
|
73
|
+
ack_mode=v2.ack_mode,
|
|
74
|
+
)
|
config/v2_config.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from config import paths
|
|
8
|
+
from modules.im.base import BaseIMConfig
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SlackConfig(BaseIMConfig):
|
|
15
|
+
bot_token: str
|
|
16
|
+
app_token: Optional[str] = None
|
|
17
|
+
signing_secret: Optional[str] = None
|
|
18
|
+
team_id: Optional[str] = None
|
|
19
|
+
team_name: Optional[str] = None
|
|
20
|
+
app_id: Optional[str] = None
|
|
21
|
+
require_mention: bool = False
|
|
22
|
+
|
|
23
|
+
def validate(self) -> None:
|
|
24
|
+
# Allow empty token for initial setup
|
|
25
|
+
if self.bot_token and not self.bot_token.startswith("xoxb-"):
|
|
26
|
+
raise ValueError("Invalid Slack bot token format (should start with xoxb-)")
|
|
27
|
+
if self.app_token and not self.app_token.startswith("xapp-"):
|
|
28
|
+
raise ValueError("Invalid Slack app token format (should start with xapp-)")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class GatewayConfig:
|
|
33
|
+
relay_url: Optional[str] = None
|
|
34
|
+
workspace_token: Optional[str] = None
|
|
35
|
+
client_id: Optional[str] = None
|
|
36
|
+
client_secret: Optional[str] = None
|
|
37
|
+
last_connected_at: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class RuntimeConfig:
|
|
42
|
+
default_cwd: str
|
|
43
|
+
log_level: str = "INFO"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class OpenCodeConfig:
|
|
49
|
+
enabled: bool = True
|
|
50
|
+
cli_path: str = "opencode"
|
|
51
|
+
default_agent: Optional[str] = None
|
|
52
|
+
default_model: Optional[str] = None
|
|
53
|
+
default_reasoning_effort: Optional[str] = None
|
|
54
|
+
error_retry_limit: int = 1 # Max retries on LLM stream errors (0 = no retry)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ClaudeConfig:
|
|
59
|
+
enabled: bool = True
|
|
60
|
+
cli_path: str = "claude"
|
|
61
|
+
default_model: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class CodexConfig:
|
|
66
|
+
enabled: bool = True
|
|
67
|
+
cli_path: str = "codex"
|
|
68
|
+
default_model: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class AgentsConfig:
|
|
73
|
+
default_backend: str = "opencode"
|
|
74
|
+
opencode: OpenCodeConfig = field(default_factory=OpenCodeConfig)
|
|
75
|
+
claude: ClaudeConfig = field(default_factory=ClaudeConfig)
|
|
76
|
+
codex: CodexConfig = field(default_factory=CodexConfig)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class UiConfig:
|
|
81
|
+
setup_host: str = "127.0.0.1"
|
|
82
|
+
setup_port: int = 5123
|
|
83
|
+
open_browser: bool = True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class V2Config:
|
|
88
|
+
mode: str
|
|
89
|
+
version: str
|
|
90
|
+
slack: SlackConfig
|
|
91
|
+
runtime: RuntimeConfig
|
|
92
|
+
agents: AgentsConfig
|
|
93
|
+
gateway: Optional[GatewayConfig] = None
|
|
94
|
+
ui: UiConfig = field(default_factory=UiConfig)
|
|
95
|
+
ack_mode: str = "reaction"
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def load(cls, config_path: Optional[Path] = None) -> "V2Config":
|
|
99
|
+
paths.ensure_data_dirs()
|
|
100
|
+
path = config_path or paths.get_config_path()
|
|
101
|
+
if not path.exists():
|
|
102
|
+
raise FileNotFoundError(f"Config not found: {path}")
|
|
103
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
104
|
+
return cls.from_payload(payload)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_payload(cls, payload: dict) -> "V2Config":
|
|
108
|
+
if not isinstance(payload, dict):
|
|
109
|
+
raise ValueError("Config payload must be an object")
|
|
110
|
+
|
|
111
|
+
mode = payload.get("mode")
|
|
112
|
+
if mode not in {"self_host", "saas"}:
|
|
113
|
+
raise ValueError("Config 'mode' must be 'self_host' or 'saas'")
|
|
114
|
+
|
|
115
|
+
slack_payload = payload.get("slack")
|
|
116
|
+
if not isinstance(slack_payload, dict):
|
|
117
|
+
raise ValueError("Config 'slack' must be an object")
|
|
118
|
+
|
|
119
|
+
if "require_mention" not in slack_payload:
|
|
120
|
+
slack_payload = dict(slack_payload)
|
|
121
|
+
slack_payload["require_mention"] = False
|
|
122
|
+
|
|
123
|
+
slack = SlackConfig(**slack_payload)
|
|
124
|
+
slack.validate()
|
|
125
|
+
gateway_payload = payload.get("gateway")
|
|
126
|
+
if gateway_payload is not None and not isinstance(gateway_payload, dict):
|
|
127
|
+
raise ValueError("Config 'gateway' must be an object")
|
|
128
|
+
gateway = GatewayConfig(**gateway_payload) if gateway_payload else None
|
|
129
|
+
|
|
130
|
+
runtime_payload = payload.get("runtime")
|
|
131
|
+
if not isinstance(runtime_payload, dict):
|
|
132
|
+
raise ValueError("Config 'runtime' must be an object")
|
|
133
|
+
runtime = RuntimeConfig(**runtime_payload)
|
|
134
|
+
|
|
135
|
+
agents_payload = payload.get("agents")
|
|
136
|
+
if not isinstance(agents_payload, dict):
|
|
137
|
+
raise ValueError("Config 'agents' must be an object")
|
|
138
|
+
|
|
139
|
+
opencode_payload = agents_payload.get("opencode") or {}
|
|
140
|
+
if not isinstance(opencode_payload, dict):
|
|
141
|
+
raise ValueError("Config 'agents.opencode' must be an object")
|
|
142
|
+
|
|
143
|
+
claude_payload = agents_payload.get("claude") or {}
|
|
144
|
+
if not isinstance(claude_payload, dict):
|
|
145
|
+
raise ValueError("Config 'agents.claude' must be an object")
|
|
146
|
+
|
|
147
|
+
codex_payload = agents_payload.get("codex") or {}
|
|
148
|
+
if not isinstance(codex_payload, dict):
|
|
149
|
+
raise ValueError("Config 'agents.codex' must be an object")
|
|
150
|
+
|
|
151
|
+
opencode = OpenCodeConfig(**opencode_payload)
|
|
152
|
+
claude = ClaudeConfig(**claude_payload)
|
|
153
|
+
codex = CodexConfig(**codex_payload)
|
|
154
|
+
|
|
155
|
+
default_backend = agents_payload.get("default_backend", "opencode")
|
|
156
|
+
if default_backend not in {"opencode", "claude", "codex"}:
|
|
157
|
+
raise ValueError("Config 'agents.default_backend' must be 'opencode', 'claude', or 'codex'")
|
|
158
|
+
|
|
159
|
+
agents = AgentsConfig(
|
|
160
|
+
default_backend=default_backend,
|
|
161
|
+
opencode=opencode,
|
|
162
|
+
claude=claude,
|
|
163
|
+
codex=codex,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
ui_payload = payload.get("ui") or {}
|
|
167
|
+
if not isinstance(ui_payload, dict):
|
|
168
|
+
raise ValueError("Config 'ui' must be an object")
|
|
169
|
+
ui = UiConfig(**ui_payload)
|
|
170
|
+
ack_mode = payload.get("ack_mode", "reaction")
|
|
171
|
+
if ack_mode not in {"reaction", "message"}:
|
|
172
|
+
raise ValueError("Config 'ack_mode' must be 'reaction' or 'message'")
|
|
173
|
+
|
|
174
|
+
return cls(
|
|
175
|
+
mode=mode,
|
|
176
|
+
version=payload.get("version", "v2"),
|
|
177
|
+
slack=slack,
|
|
178
|
+
runtime=runtime,
|
|
179
|
+
agents=agents,
|
|
180
|
+
gateway=gateway,
|
|
181
|
+
ui=ui,
|
|
182
|
+
ack_mode=ack_mode,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def save(self, config_path: Optional[Path] = None) -> None:
|
|
186
|
+
paths.ensure_data_dirs()
|
|
187
|
+
path = config_path or paths.get_config_path()
|
|
188
|
+
payload = {
|
|
189
|
+
"mode": self.mode,
|
|
190
|
+
"version": self.version,
|
|
191
|
+
"slack": self.slack.__dict__,
|
|
192
|
+
"runtime": {
|
|
193
|
+
"default_cwd": self.runtime.default_cwd,
|
|
194
|
+
"log_level": self.runtime.log_level,
|
|
195
|
+
},
|
|
196
|
+
"agents": {
|
|
197
|
+
"default_backend": self.agents.default_backend,
|
|
198
|
+
"opencode": self.agents.opencode.__dict__,
|
|
199
|
+
"claude": self.agents.claude.__dict__,
|
|
200
|
+
"codex": self.agents.codex.__dict__,
|
|
201
|
+
},
|
|
202
|
+
"gateway": self.gateway.__dict__ if self.gateway else None,
|
|
203
|
+
"ui": self.ui.__dict__,
|
|
204
|
+
"ack_mode": self.ack_mode,
|
|
205
|
+
}
|
|
206
|
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
config/v2_sessions.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
from config import paths
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SessionState:
|
|
14
|
+
# session_mappings: user_id -> agent_name -> thread_id -> session_id
|
|
15
|
+
session_mappings: Dict[str, Dict[str, Dict[str, str]]] = field(
|
|
16
|
+
default_factory=dict
|
|
17
|
+
)
|
|
18
|
+
active_slack_threads: Dict[str, Dict[str, Dict[str, float]]] = field(
|
|
19
|
+
default_factory=dict
|
|
20
|
+
)
|
|
21
|
+
last_activity: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SessionsStore:
|
|
26
|
+
sessions_path: Path = field(default_factory=paths.get_sessions_path)
|
|
27
|
+
state: SessionState = field(default_factory=SessionState)
|
|
28
|
+
|
|
29
|
+
def load(self) -> None:
|
|
30
|
+
if not self.sessions_path.exists():
|
|
31
|
+
return
|
|
32
|
+
try:
|
|
33
|
+
payload = json.loads(self.sessions_path.read_text(encoding="utf-8"))
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
logger.error("Failed to load sessions: %s", exc)
|
|
36
|
+
return
|
|
37
|
+
self.state = SessionState(
|
|
38
|
+
session_mappings=payload.get("session_mappings", {}),
|
|
39
|
+
active_slack_threads=payload.get("active_slack_threads", {}),
|
|
40
|
+
last_activity=payload.get("last_activity"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def _ensure_user_namespace(self, user_id: str) -> None:
|
|
44
|
+
if user_id not in self.state.session_mappings:
|
|
45
|
+
self.state.session_mappings[user_id] = {}
|
|
46
|
+
if user_id not in self.state.active_slack_threads:
|
|
47
|
+
self.state.active_slack_threads[user_id] = {}
|
|
48
|
+
|
|
49
|
+
def get_agent_map(self, user_id: str, agent_name: str) -> Dict[str, str]:
|
|
50
|
+
"""Get mapping of thread_id -> session_id for a user and agent."""
|
|
51
|
+
self._ensure_user_namespace(user_id)
|
|
52
|
+
agent_map = self.state.session_mappings[user_id].get(agent_name)
|
|
53
|
+
if agent_map is None:
|
|
54
|
+
agent_map = {}
|
|
55
|
+
self.state.session_mappings[user_id][agent_name] = agent_map
|
|
56
|
+
return agent_map
|
|
57
|
+
|
|
58
|
+
def get_thread_map(self, user_id: str, channel_id: str) -> Dict[str, float]:
|
|
59
|
+
self._ensure_user_namespace(user_id)
|
|
60
|
+
channel_map = self.state.active_slack_threads[user_id].get(channel_id)
|
|
61
|
+
if channel_map is None:
|
|
62
|
+
channel_map = {}
|
|
63
|
+
self.state.active_slack_threads[user_id][channel_id] = channel_map
|
|
64
|
+
return channel_map
|
|
65
|
+
|
|
66
|
+
def save(self) -> None:
|
|
67
|
+
paths.ensure_data_dirs()
|
|
68
|
+
payload = {
|
|
69
|
+
"session_mappings": self.state.session_mappings,
|
|
70
|
+
"active_slack_threads": self.state.active_slack_threads,
|
|
71
|
+
"last_activity": self.state.last_activity,
|
|
72
|
+
}
|
|
73
|
+
self.sessions_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
config/v2_settings.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from config import paths
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
DEFAULT_SHOW_MESSAGE_TYPES: List[str] = []
|
|
13
|
+
ALLOWED_MESSAGE_TYPES = {"system", "assistant", "toolcall"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def normalize_show_message_types(show_message_types: Optional[List[str]]) -> List[str]:
|
|
17
|
+
if show_message_types is None:
|
|
18
|
+
return DEFAULT_SHOW_MESSAGE_TYPES.copy()
|
|
19
|
+
return [msg for msg in show_message_types if msg in ALLOWED_MESSAGE_TYPES]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class RoutingSettings:
|
|
24
|
+
agent_backend: Optional[str] = None
|
|
25
|
+
opencode_agent: Optional[str] = None
|
|
26
|
+
opencode_model: Optional[str] = None
|
|
27
|
+
opencode_reasoning_effort: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ChannelSettings:
|
|
32
|
+
enabled: bool = False
|
|
33
|
+
show_message_types: List[str] = field(
|
|
34
|
+
default_factory=lambda: DEFAULT_SHOW_MESSAGE_TYPES.copy()
|
|
35
|
+
)
|
|
36
|
+
custom_cwd: Optional[str] = None
|
|
37
|
+
routing: RoutingSettings = field(default_factory=RoutingSettings)
|
|
38
|
+
# Per-channel require_mention override: None=use global default, True=require, False=don't require
|
|
39
|
+
require_mention: Optional[bool] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class SettingsState:
|
|
44
|
+
channels: Dict[str, ChannelSettings] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SettingsStore:
|
|
48
|
+
def __init__(self, settings_path: Optional[Path] = None):
|
|
49
|
+
self.settings_path = settings_path or paths.get_settings_path()
|
|
50
|
+
self.settings: SettingsState = SettingsState()
|
|
51
|
+
self._load()
|
|
52
|
+
|
|
53
|
+
def _load(self) -> None:
|
|
54
|
+
if not self.settings_path.exists():
|
|
55
|
+
return
|
|
56
|
+
try:
|
|
57
|
+
payload = json.loads(self.settings_path.read_text(encoding="utf-8"))
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
logger.error("Failed to load settings: %s", exc)
|
|
60
|
+
return
|
|
61
|
+
raw_channels = payload.get("channels") if isinstance(payload, dict) else None
|
|
62
|
+
if raw_channels is None:
|
|
63
|
+
logger.error("Failed to load settings: invalid format")
|
|
64
|
+
return
|
|
65
|
+
if not isinstance(raw_channels, dict):
|
|
66
|
+
logger.error("Failed to load settings: channels must be an object")
|
|
67
|
+
return
|
|
68
|
+
channels = {}
|
|
69
|
+
for channel_id, channel_payload in raw_channels.items():
|
|
70
|
+
if not isinstance(channel_payload, dict):
|
|
71
|
+
continue
|
|
72
|
+
routing_payload = channel_payload.get("routing") or {}
|
|
73
|
+
routing = RoutingSettings(
|
|
74
|
+
agent_backend=routing_payload.get("agent_backend"),
|
|
75
|
+
opencode_agent=routing_payload.get("opencode_agent"),
|
|
76
|
+
opencode_model=routing_payload.get("opencode_model"),
|
|
77
|
+
opencode_reasoning_effort=routing_payload.get("opencode_reasoning_effort"),
|
|
78
|
+
)
|
|
79
|
+
channels[channel_id] = ChannelSettings(
|
|
80
|
+
enabled=channel_payload.get("enabled", False),
|
|
81
|
+
show_message_types=normalize_show_message_types(
|
|
82
|
+
channel_payload.get("show_message_types")
|
|
83
|
+
),
|
|
84
|
+
custom_cwd=channel_payload.get("custom_cwd"),
|
|
85
|
+
routing=routing,
|
|
86
|
+
require_mention=channel_payload.get("require_mention"),
|
|
87
|
+
)
|
|
88
|
+
self.settings = SettingsState(channels=channels)
|
|
89
|
+
|
|
90
|
+
def save(self) -> None:
|
|
91
|
+
paths.ensure_data_dirs()
|
|
92
|
+
payload = {"channels": {}}
|
|
93
|
+
for channel_id, settings in self.settings.channels.items():
|
|
94
|
+
payload["channels"][channel_id] = {
|
|
95
|
+
"enabled": settings.enabled,
|
|
96
|
+
"show_message_types": settings.show_message_types,
|
|
97
|
+
"custom_cwd": settings.custom_cwd,
|
|
98
|
+
"routing": {
|
|
99
|
+
"agent_backend": settings.routing.agent_backend,
|
|
100
|
+
"opencode_agent": settings.routing.opencode_agent,
|
|
101
|
+
"opencode_model": settings.routing.opencode_model,
|
|
102
|
+
"opencode_reasoning_effort": settings.routing.opencode_reasoning_effort,
|
|
103
|
+
},
|
|
104
|
+
"require_mention": settings.require_mention,
|
|
105
|
+
}
|
|
106
|
+
self.settings_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
107
|
+
|
|
108
|
+
def get_channel(self, channel_id: str) -> ChannelSettings:
|
|
109
|
+
if channel_id not in self.settings.channels:
|
|
110
|
+
self.settings.channels[channel_id] = ChannelSettings()
|
|
111
|
+
return self.settings.channels[channel_id]
|
|
112
|
+
|
|
113
|
+
def update_channel(self, channel_id: str, settings: ChannelSettings) -> None:
|
|
114
|
+
self.settings.channels[channel_id] = settings
|
|
115
|
+
self.save()
|
core/__init__.py
ADDED
|
File without changes
|