calvyn-code 0.14.9 → 0.14.11
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.
- package/cli.py +80 -62
- package/hermes_cli/__init__.py +8 -8
- package/hermes_cli/commands.py +8 -7
- package/hermes_cli/model_switch.py +17 -1
- package/hermes_cli/models.py +8 -5
- package/package.json +3 -2
- package/plugins/platforms/vk/__init__.py +3 -0
- package/plugins/platforms/vk/adapter.py +105 -0
- package/plugins/platforms/vk/plugin.yaml +25 -0
- package/pyproject.toml +1 -1
- package/tools/lazy_deps.py +7 -4
package/cli.py
CHANGED
|
@@ -46,7 +46,10 @@ from pathlib import Path
|
|
|
46
46
|
from datetime import datetime
|
|
47
47
|
from typing import List, Dict, Any, Optional
|
|
48
48
|
|
|
49
|
-
logger = logging.getLogger(__name__)
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
_DEFAULT_INVITE_CODE = "CALVYN-7F29D4-OWNER"
|
|
52
|
+
_DEFAULT_INVITE_CODE_HASH = hashlib.sha256(_DEFAULT_INVITE_CODE.encode("utf-8")).hexdigest()
|
|
50
53
|
|
|
51
54
|
# Suppress startup messages for clean CLI experience
|
|
52
55
|
os.environ["HERMES_QUIET"] = "1" # Our own modules
|
|
@@ -2516,47 +2519,48 @@ def _ensure_dev_access_gate() -> None:
|
|
|
2516
2519
|
return
|
|
2517
2520
|
|
|
2518
2521
|
access_code_hash = str(security_cfg.get("dev_access_code_hash") or "").strip().lower()
|
|
2519
|
-
|
|
2522
|
+
if not access_code_hash:
|
|
2523
|
+
access_code_hash = _DEFAULT_INVITE_CODE_HASH
|
|
2524
|
+
save_config_value("security.dev_access_code_hash", access_code_hash)
|
|
2520
2525
|
|
|
2521
2526
|
print()
|
|
2522
2527
|
print("╔══════════════════════════════════════════════════════════════════════════════╗")
|
|
2523
2528
|
print("║ CALVYN DEV АВТОРИЗАЦИЯ ║")
|
|
2524
2529
|
print("╠══════════════════════════════════════════════════════════════════════════════╣")
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
print("║ Сам код нигде не хранится в открытом виде и не может быть прочитан обратно.║")
|
|
2528
|
-
else:
|
|
2529
|
-
print("║ Доступ к режиму разработки защищен вашим секретным кодом. ║")
|
|
2530
|
-
print("║ В системе хранится только хэш. Введите код для разблокировки. ║")
|
|
2530
|
+
print("║ Доступ к режиму разработки открыт только по приглашению владельца. ║")
|
|
2531
|
+
print("║ В системе хранится только хэш секретного кода. Открытый код не сохраняется.║")
|
|
2531
2532
|
print("╚══════════════════════════════════════════════════════════════════════════════╝")
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
raise SystemExit("Код должен быть не короче 8 символов.")
|
|
2537
|
-
if entered != confirm:
|
|
2538
|
-
raise SystemExit("Коды не совпадают.")
|
|
2539
|
-
access_code_hash = hashlib.sha256(entered.encode("utf-8")).hexdigest()
|
|
2540
|
-
save_config_value("security.dev_access_code_hash", access_code_hash)
|
|
2541
|
-
else:
|
|
2542
|
-
entered = input("Код доступа: ").strip()
|
|
2543
|
-
entered_hash = hashlib.sha256(entered.encode("utf-8")).hexdigest()
|
|
2544
|
-
if entered_hash != access_code_hash:
|
|
2545
|
-
raise SystemExit("Неверный код доступа.")
|
|
2533
|
+
entered = input("Код доступа: ").strip()
|
|
2534
|
+
entered_hash = hashlib.sha256(entered.encode("utf-8")).hexdigest()
|
|
2535
|
+
if entered_hash != access_code_hash:
|
|
2536
|
+
raise SystemExit("Неверный код доступа.")
|
|
2546
2537
|
save_config_value("security.dev_access_granted", True)
|
|
2547
2538
|
except SystemExit:
|
|
2548
2539
|
raise
|
|
2549
2540
|
except Exception as exc:
|
|
2550
2541
|
print(f"⚠ Dev-доступ не проверен: {exc}")
|
|
2542
|
+
|
|
2543
|
+
|
|
2544
|
+
def _show_early_startup_logo() -> None:
|
|
2545
|
+
"""Render the large startup logo before auth/init so all launch paths match."""
|
|
2546
|
+
try:
|
|
2547
|
+
if os.getenv("CALVYN_EARLY_BANNER_SHOWN", "").strip().lower() in {"1", "true", "yes", "on"}:
|
|
2548
|
+
return
|
|
2549
|
+
from rich.console import Console
|
|
2550
|
+
console = Console()
|
|
2551
|
+
console.print(HERMES_AGENT_LOGO)
|
|
2552
|
+
os.environ["CALVYN_EARLY_BANNER_SHOWN"] = "1"
|
|
2553
|
+
except Exception:
|
|
2554
|
+
pass
|
|
2551
2555
|
|
|
2552
2556
|
|
|
2553
2557
|
|
|
2554
2558
|
|
|
2555
2559
|
# ============================================================================
|
|
2556
|
-
#
|
|
2560
|
+
# CalvynCLI Class
|
|
2557
2561
|
# ============================================================================
|
|
2558
2562
|
|
|
2559
|
-
class HermesCLI:
|
|
2563
|
+
class HermesCLI:
|
|
2560
2564
|
"""
|
|
2561
2565
|
Interactive CLI for Calvyn Code.
|
|
2562
2566
|
|
|
@@ -2675,7 +2679,7 @@ class HermesCLI:
|
|
|
2675
2679
|
# env vars would stomp each other.
|
|
2676
2680
|
_model_config = CLI_CONFIG.get("model", {})
|
|
2677
2681
|
_config_model = (_model_config.get("default") or _model_config.get("model") or "") if isinstance(_model_config, dict) else (_model_config or "")
|
|
2678
|
-
_DEFAULT_CONFIG_MODEL = ""
|
|
2682
|
+
_DEFAULT_CONFIG_MODEL = "gpt-5.3-codex"
|
|
2679
2683
|
self.model = model or _config_model or _DEFAULT_CONFIG_MODEL
|
|
2680
2684
|
# Auto-detect model from local server if still on default
|
|
2681
2685
|
if self.model == _DEFAULT_CONFIG_MODEL:
|
|
@@ -4614,7 +4618,10 @@ class HermesCLI:
|
|
|
4614
4618
|
self._console_print()
|
|
4615
4619
|
self._console_print()
|
|
4616
4620
|
term_width = shutil.get_terminal_size().columns
|
|
4617
|
-
|
|
4621
|
+
_early_banner_shown = os.getenv("CALVYN_EARLY_BANNER_SHOWN", "").strip().lower() in {
|
|
4622
|
+
"1", "true", "yes", "on",
|
|
4623
|
+
}
|
|
4624
|
+
if term_width >= 95 and not _early_banner_shown:
|
|
4618
4625
|
self._console_print(_logo)
|
|
4619
4626
|
self._console_print()
|
|
4620
4627
|
self._console_print(_build_compact_banner())
|
|
@@ -6446,7 +6453,7 @@ class HermesCLI:
|
|
|
6446
6453
|
}, f, indent=2, ensure_ascii=False)
|
|
6447
6454
|
print(f"(^_^)v Conversation snapshot saved to: {path}")
|
|
6448
6455
|
if self.session_id:
|
|
6449
|
-
print(f" Resume the live session with:
|
|
6456
|
+
print(f" Resume the live session with: calvyn --resume {self.session_id}")
|
|
6450
6457
|
except Exception as e:
|
|
6451
6458
|
print(f"(x_x) Failed to save: {e}")
|
|
6452
6459
|
|
|
@@ -6897,13 +6904,15 @@ class HermesCLI:
|
|
|
6897
6904
|
_cprint(" Prompt caching: enabled")
|
|
6898
6905
|
if result.warning_message:
|
|
6899
6906
|
_cprint(f" ⚠ {result.warning_message}")
|
|
6900
|
-
if persist_global:
|
|
6901
|
-
save_config_value("model.default", result.new_model)
|
|
6902
|
-
if result.provider_changed:
|
|
6903
|
-
save_config_value("model.provider", result.target_provider)
|
|
6904
|
-
_cprint(" Saved to config.yaml (--global)")
|
|
6905
|
-
else:
|
|
6906
|
-
|
|
6907
|
+
if persist_global:
|
|
6908
|
+
save_config_value("model.default", result.new_model)
|
|
6909
|
+
if result.provider_changed:
|
|
6910
|
+
save_config_value("model.provider", result.target_provider)
|
|
6911
|
+
_cprint(" Saved to config.yaml (--global)")
|
|
6912
|
+
else:
|
|
6913
|
+
save_config_value("model.default", result.new_model)
|
|
6914
|
+
save_config_value("model.provider", result.target_provider)
|
|
6915
|
+
_cprint(" Saved to config.yaml")
|
|
6907
6916
|
|
|
6908
6917
|
def _handle_model_picker_selection(self, persist_global: bool = False) -> None:
|
|
6909
6918
|
state = self._model_picker_state
|
|
@@ -7140,13 +7149,15 @@ class HermesCLI:
|
|
|
7140
7149
|
_cprint(f" ⚠ {result.warning_message}")
|
|
7141
7150
|
|
|
7142
7151
|
# Persistence
|
|
7143
|
-
if persist_global:
|
|
7144
|
-
save_config_value("model.default", result.new_model)
|
|
7145
|
-
if result.provider_changed:
|
|
7146
|
-
save_config_value("model.provider", result.target_provider)
|
|
7147
|
-
_cprint(" Saved to config.yaml (--global)")
|
|
7148
|
-
else:
|
|
7149
|
-
|
|
7152
|
+
if persist_global:
|
|
7153
|
+
save_config_value("model.default", result.new_model)
|
|
7154
|
+
if result.provider_changed:
|
|
7155
|
+
save_config_value("model.provider", result.target_provider)
|
|
7156
|
+
_cprint(" Saved to config.yaml (--global)")
|
|
7157
|
+
else:
|
|
7158
|
+
save_config_value("model.default", result.new_model)
|
|
7159
|
+
save_config_value("model.provider", result.target_provider)
|
|
7160
|
+
_cprint(" Saved to config.yaml")
|
|
7150
7161
|
|
|
7151
7162
|
def _handle_codex_runtime(self, cmd_original: str) -> None:
|
|
7152
7163
|
"""Handle /codex-runtime — toggle the codex app-server runtime opt-in.
|
|
@@ -7708,12 +7719,16 @@ class HermesCLI:
|
|
|
7708
7719
|
_cmd_def = _resolve_cmd(_base_word)
|
|
7709
7720
|
canonical = _cmd_def.name if _cmd_def else _base_word
|
|
7710
7721
|
|
|
7711
|
-
if canonical in {"quit", "exit"}:
|
|
7712
|
-
return False
|
|
7713
|
-
elif canonical == "help":
|
|
7714
|
-
self.show_help()
|
|
7715
|
-
elif canonical == "
|
|
7716
|
-
|
|
7722
|
+
if canonical in {"quit", "exit"}:
|
|
7723
|
+
return False
|
|
7724
|
+
elif canonical == "help":
|
|
7725
|
+
self.show_help()
|
|
7726
|
+
elif canonical == "author":
|
|
7727
|
+
_cprint(" Автор проекта: Zanderrr")
|
|
7728
|
+
_cprint(" VK: vk.com/zanderr_r")
|
|
7729
|
+
_cprint(" Это приватная developer-сборка Calvyn Code.")
|
|
7730
|
+
elif canonical == "profile":
|
|
7731
|
+
self._handle_profile_command()
|
|
7717
7732
|
elif canonical == "tools":
|
|
7718
7733
|
self._handle_tools_command(cmd_original)
|
|
7719
7734
|
elif canonical == "toolsets":
|
|
@@ -8610,7 +8625,7 @@ class HermesCLI:
|
|
|
8610
8625
|
_cprint(f" ⊙ Goal set ({state.max_turns}-turn budget): {state.goal}")
|
|
8611
8626
|
_cprint(
|
|
8612
8627
|
f" {_DIM}After each turn, a judge model will check if the goal is done. "
|
|
8613
|
-
f"
|
|
8628
|
+
f"Calvyn продолжит работу, пока цель не будет выполнена, пока вы не поставите паузу/очистку, или пока не закончится бюджет. "
|
|
8614
8629
|
f"exhausted. Use /goal status, /goal pause, /goal resume, /goal clear.{_RST}"
|
|
8615
8630
|
)
|
|
8616
8631
|
# Kick the loop off immediately so the user doesn't have to send a
|
|
@@ -9014,7 +9029,7 @@ class HermesCLI:
|
|
|
9014
9029
|
_cprint(f" {_ACCENT}✓ Reasoning effort set to '{arg}' (session only){_RST}")
|
|
9015
9030
|
|
|
9016
9031
|
def _handle_busy_command(self, cmd: str):
|
|
9017
|
-
"""Handle /busy — control what Enter does while
|
|
9032
|
+
"""Handle /busy — control what Enter does while Calvyn is working.
|
|
9018
9033
|
|
|
9019
9034
|
Usage:
|
|
9020
9035
|
/busy Show current busy input mode
|
|
@@ -9045,11 +9060,11 @@ class HermesCLI:
|
|
|
9045
9060
|
self.busy_input_mode = arg
|
|
9046
9061
|
if save_config_value("display.busy_input_mode", arg):
|
|
9047
9062
|
if arg == "queue":
|
|
9048
|
-
behavior = "Enter will queue follow-up input while
|
|
9063
|
+
behavior = "Enter will queue follow-up input while Calvyn is busy."
|
|
9049
9064
|
elif arg == "steer":
|
|
9050
9065
|
behavior = "Enter will steer your message into the current run (after the next tool call)."
|
|
9051
9066
|
else:
|
|
9052
|
-
behavior = "Enter will interrupt the current run while
|
|
9067
|
+
behavior = "Enter will interrupt the current run while Calvyn is busy."
|
|
9053
9068
|
_cprint(f" {_ACCENT}✓ Busy input mode set to '{arg}' (saved to config){_RST}")
|
|
9054
9069
|
_cprint(f" {_DIM}{behavior}{_RST}")
|
|
9055
9070
|
else:
|
|
@@ -11354,9 +11369,9 @@ class HermesCLI:
|
|
|
11354
11369
|
pass
|
|
11355
11370
|
|
|
11356
11371
|
print("Resume this session with:")
|
|
11357
|
-
print(f"
|
|
11358
|
-
if session_title:
|
|
11359
|
-
print(f"
|
|
11372
|
+
print(f" calvyn --resume {self.session_id}")
|
|
11373
|
+
if session_title:
|
|
11374
|
+
print(f" calvyn -c \"{session_title}\"")
|
|
11360
11375
|
print()
|
|
11361
11376
|
print(f"Session: {self.session_id}")
|
|
11362
11377
|
if session_title:
|
|
@@ -13972,13 +13987,16 @@ def main(
|
|
|
13972
13987
|
# Force UTF-8 stdio on Windows before any banner/print() runs — the
|
|
13973
13988
|
# Rich console prints Unicode box-drawing characters that would
|
|
13974
13989
|
# UnicodeEncodeError on cp1252. No-op on Linux/macOS.
|
|
13975
|
-
try:
|
|
13976
|
-
from hermes_cli.stdio import configure_windows_stdio
|
|
13977
|
-
configure_windows_stdio()
|
|
13978
|
-
except Exception:
|
|
13979
|
-
pass
|
|
13980
|
-
|
|
13981
|
-
|
|
13990
|
+
try:
|
|
13991
|
+
from hermes_cli.stdio import configure_windows_stdio
|
|
13992
|
+
configure_windows_stdio()
|
|
13993
|
+
except Exception:
|
|
13994
|
+
pass
|
|
13995
|
+
|
|
13996
|
+
if not quiet and not gateway and not query and not q and not image and not list_tools and not list_toolsets:
|
|
13997
|
+
_show_early_startup_logo()
|
|
13998
|
+
|
|
13999
|
+
# Signal to terminal_tool that we're in interactive mode
|
|
13982
14000
|
# This enables interactive sudo password prompts with timeout
|
|
13983
14001
|
os.environ["HERMES_INTERACTIVE"] = "1"
|
|
13984
14002
|
|
|
@@ -13986,7 +14004,7 @@ def main(
|
|
|
13986
14004
|
if gateway:
|
|
13987
14005
|
import asyncio
|
|
13988
14006
|
from gateway.run import start_gateway
|
|
13989
|
-
print("Starting
|
|
14007
|
+
print("Starting Calvyn Gateway (messaging platforms)...")
|
|
13990
14008
|
asyncio.run(start_gateway())
|
|
13991
14009
|
return
|
|
13992
14010
|
|
package/hermes_cli/__init__.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Calvyn Code CLI - Unified command-line interface for Calvyn Code.
|
|
3
3
|
|
|
4
|
-
Provides subcommands for:
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
4
|
+
Provides subcommands for:
|
|
5
|
+
- calvyn chat - Interactive chat (same as ./calvyn)
|
|
6
|
+
- calvyn gateway - Run gateway in foreground
|
|
7
|
+
- calvyn gateway start - Start gateway service
|
|
8
|
+
- calvyn gateway stop - Stop gateway service
|
|
9
|
+
- calvyn setup - Interactive setup wizard
|
|
10
|
+
- calvyn status - Show status of all components
|
|
11
|
+
- calvyn cron - Manage cron jobs
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
14
|
import os
|
package/hermes_cli/commands.py
CHANGED
|
@@ -191,8 +191,9 @@ COMMAND_REGISTRY: list[CommandDef] = [
|
|
|
191
191
|
CommandDef("commands", "Открыть все команды и skills постранично", "Info",
|
|
192
192
|
gateway_only=True, args_hint="[page]"),
|
|
193
193
|
CommandDef("help", "Показать доступные команды", "Info"),
|
|
194
|
+
CommandDef("author", "Показать автора и контакты проекта", "Info"),
|
|
194
195
|
CommandDef("restart", "Аккуратно перезапустить gateway после завершения активных задач", "Session",
|
|
195
|
-
gateway_only=True),
|
|
196
|
+
gateway_only=True),
|
|
196
197
|
CommandDef("usage", "Показать расход токенов и лимиты", "Info"),
|
|
197
198
|
CommandDef("insights", "Показать статистику использования", "Info",
|
|
198
199
|
args_hint="[days]"),
|
|
@@ -973,20 +974,20 @@ def slack_native_slashes() -> list[tuple[str, str, str]]:
|
|
|
973
974
|
|
|
974
975
|
Commands whose sanitized name collides with a Slack built-in
|
|
975
976
|
(e.g. ``/status``, ``/me``, ``/join``) are silently skipped. Users
|
|
976
|
-
can still reach them via ``/
|
|
977
|
+
can still reach them via ``/calvyn <command>``.
|
|
977
978
|
|
|
978
979
|
Results are clamped to Slack's 50-command limit with duplicate-name
|
|
979
|
-
avoidance. ``/
|
|
980
|
-
legacy ``/
|
|
980
|
+
avoidance. ``/calvyn`` is always reserved as the first entry so the
|
|
981
|
+
legacy ``/calvyn <subcommand>`` form keeps working for anything that
|
|
981
982
|
gets dropped by the clamp or for free-form questions.
|
|
982
983
|
"""
|
|
983
984
|
overrides = _resolve_config_gates()
|
|
984
985
|
entries: list[tuple[str, str, str]] = []
|
|
985
986
|
seen: set[str] = set()
|
|
986
987
|
|
|
987
|
-
# Reserve /
|
|
988
|
-
entries.append(("
|
|
989
|
-
seen.add("
|
|
988
|
+
# Reserve /calvyn as the catch-all top-level command.
|
|
989
|
+
entries.append(("calvyn", "Запустить команду или задать вопрос Calvyn", "[subcommand] [args]"))
|
|
990
|
+
seen.add("calvyn")
|
|
990
991
|
|
|
991
992
|
def _add(name: str, desc: str, hint: str) -> None:
|
|
992
993
|
slack_name = _sanitize_slack_name(name)
|
|
@@ -170,7 +170,23 @@ class DirectAlias(NamedTuple):
|
|
|
170
170
|
|
|
171
171
|
|
|
172
172
|
# Built-in direct aliases (can be extended via config.yaml model_aliases:)
|
|
173
|
-
_BUILTIN_DIRECT_ALIASES: dict[str, DirectAlias] = {
|
|
173
|
+
_BUILTIN_DIRECT_ALIASES: dict[str, DirectAlias] = {
|
|
174
|
+
"freemodel/gpt": DirectAlias(
|
|
175
|
+
model="gpt-5.3-codex",
|
|
176
|
+
provider="freemodel",
|
|
177
|
+
base_url="https://freemodel.dev/v1",
|
|
178
|
+
),
|
|
179
|
+
"freemodel/gpt-5.3-codex": DirectAlias(
|
|
180
|
+
model="gpt-5.3-codex",
|
|
181
|
+
provider="freemodel",
|
|
182
|
+
base_url="https://freemodel.dev/v1",
|
|
183
|
+
),
|
|
184
|
+
"openai/gpt": DirectAlias(
|
|
185
|
+
model="gpt-5.4",
|
|
186
|
+
provider="openai",
|
|
187
|
+
base_url="",
|
|
188
|
+
),
|
|
189
|
+
}
|
|
174
190
|
|
|
175
191
|
# Merged dict (builtins + user config); populated by _load_direct_aliases()
|
|
176
192
|
DIRECT_ALIASES: dict[str, DirectAlias] = {}
|
package/hermes_cli/models.py
CHANGED
|
@@ -189,11 +189,12 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
|
|
189
189
|
],
|
|
190
190
|
# Native OpenAI Chat Completions (api.openai.com). Used by /model counts and
|
|
191
191
|
# provider_model_ids fallback when /v1/models is unavailable.
|
|
192
|
-
"openai": [
|
|
193
|
-
"gpt
|
|
194
|
-
"gpt-5.4
|
|
195
|
-
"gpt-5-mini",
|
|
196
|
-
"gpt-5
|
|
192
|
+
"openai": [
|
|
193
|
+
"openai/gpt",
|
|
194
|
+
"gpt-5.4",
|
|
195
|
+
"gpt-5.4-mini",
|
|
196
|
+
"gpt-5-mini",
|
|
197
|
+
"gpt-5.3-codex",
|
|
197
198
|
"gpt-5.2-codex",
|
|
198
199
|
"gpt-4.1",
|
|
199
200
|
"gpt-4o",
|
|
@@ -340,6 +341,8 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
|
|
340
341
|
"openai/gpt-5.4",
|
|
341
342
|
],
|
|
342
343
|
"freemodel": [
|
|
344
|
+
"freemodel/gpt",
|
|
345
|
+
"freemodel/gpt-5.3-codex",
|
|
343
346
|
"gpt-5.5",
|
|
344
347
|
"gpt-5.4",
|
|
345
348
|
"gpt-5.4-mini",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "calvyn-code",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.11",
|
|
4
4
|
"description": "Calvyn Code — AI агент с инструментами, мессенджерами и локальным CLI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"calvyn": "bin/calvyn.js",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"url": "https://github.com/calvyns/calvyn-code/issues"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"agent-browser": "^0.26.0"
|
|
35
|
+
"agent-browser": "^0.26.0",
|
|
36
|
+
"calvyn-code": "^0.14.9"
|
|
36
37
|
},
|
|
37
38
|
"overrides": {
|
|
38
39
|
"lodash": "4.18.1"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from vkbottle.bot import Bot
|
|
10
|
+
from vkbottle import Message
|
|
11
|
+
VKBOTTLE_AVAILABLE = True
|
|
12
|
+
except Exception:
|
|
13
|
+
Bot = Any
|
|
14
|
+
Message = Any
|
|
15
|
+
VKBOTTLE_AVAILABLE = False
|
|
16
|
+
|
|
17
|
+
from gateway.config import Platform, PlatformConfig
|
|
18
|
+
from gateway.platforms.base import BasePlatformAdapter, MessageEvent, MessageType, SendResult
|
|
19
|
+
from gateway.session import SessionSource
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_requirements() -> bool:
|
|
23
|
+
if VKBOTTLE_AVAILABLE:
|
|
24
|
+
return True
|
|
25
|
+
try:
|
|
26
|
+
from tools.lazy_deps import ensure as _ensure
|
|
27
|
+
_ensure("platform.vk", prompt=False)
|
|
28
|
+
except Exception:
|
|
29
|
+
return False
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VKAdapter(BasePlatformAdapter):
|
|
34
|
+
def __init__(self, config):
|
|
35
|
+
super().__init__(config=config, platform=Platform("vk"))
|
|
36
|
+
extra = getattr(config, "extra", {}) or {}
|
|
37
|
+
self.token = os.getenv("VK_BOT_TOKEN") or getattr(config, "token", "") or extra.get("token", "")
|
|
38
|
+
self.allowed_users = {str(x).strip() for x in (os.getenv("VK_ALLOWED_USERS", "") or extra.get("allowed_users", "") or "").split(",") if str(x).strip()}
|
|
39
|
+
self.allow_all = str(os.getenv("VK_ALLOW_ALL_USERS", "") or extra.get("allow_all_users", "") or "").lower() in {"1","true","yes","on"}
|
|
40
|
+
self.home_channel = os.getenv("VK_HOME_CHANNEL") or extra.get("home_channel", "")
|
|
41
|
+
self._bot = None
|
|
42
|
+
self._task = None
|
|
43
|
+
|
|
44
|
+
async def connect(self) -> bool:
|
|
45
|
+
if not self.token:
|
|
46
|
+
self._set_fatal_error("missing_token", "VK_BOT_TOKEN is required", retryable=False)
|
|
47
|
+
return False
|
|
48
|
+
self._bot = Bot(token=self.token)
|
|
49
|
+
|
|
50
|
+
@self._bot.on.message()
|
|
51
|
+
async def _on_message(message: Message):
|
|
52
|
+
if getattr(message, "out", False):
|
|
53
|
+
return
|
|
54
|
+
user_id = str(getattr(message, "from_id", "") or "")
|
|
55
|
+
if not self.allow_all and self.allowed_users and user_id not in self.allowed_users:
|
|
56
|
+
return
|
|
57
|
+
text = (getattr(message, "text", "") or "").strip()
|
|
58
|
+
if not text:
|
|
59
|
+
return
|
|
60
|
+
peer_id = str(getattr(message, "peer_id", "") or "")
|
|
61
|
+
source = SessionSource(platform=Platform("vk"), chat_id=peer_id, user_id=user_id, chat_type="dm" if peer_id and peer_id.isdigit() and int(peer_id) < 2_000_000_000 else "group")
|
|
62
|
+
event = MessageEvent(text=text, message_type=MessageType.TEXT, source=source, raw_message=message, message_id=str(getattr(message, "conversation_message_id", "") or ""))
|
|
63
|
+
await self.handle_message(event)
|
|
64
|
+
|
|
65
|
+
self._task = asyncio.create_task(self._bot.run_forever())
|
|
66
|
+
self._mark_connected()
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
async def disconnect(self) -> None:
|
|
70
|
+
self._mark_disconnected()
|
|
71
|
+
if self._task and not self._task.done():
|
|
72
|
+
self._task.cancel()
|
|
73
|
+
self._task = None
|
|
74
|
+
|
|
75
|
+
async def send(self, chat_id: str, content: str, reply_to: Optional[str] = None, metadata: Optional[dict] = None, **kwargs) -> SendResult:
|
|
76
|
+
if not self._bot:
|
|
77
|
+
return SendResult(success=False, error="VK bot not connected")
|
|
78
|
+
try:
|
|
79
|
+
await self._bot.api.messages.send(peer_id=int(chat_id), random_id=0, message=content)
|
|
80
|
+
return SendResult(success=True, message_id="vk")
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
return SendResult(success=False, error=str(exc))
|
|
83
|
+
|
|
84
|
+
async def send_typing(self, chat_id: str) -> None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
async def get_chat_info(self, chat_id: str) -> dict:
|
|
88
|
+
return {"name": chat_id, "type": "dm", "chat_id": chat_id}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def register(ctx):
|
|
92
|
+
ctx.register_platform(
|
|
93
|
+
name="vk",
|
|
94
|
+
label="VK",
|
|
95
|
+
adapter_factory=lambda cfg: VKAdapter(cfg),
|
|
96
|
+
check_fn=check_requirements,
|
|
97
|
+
required_env=["VK_BOT_TOKEN"],
|
|
98
|
+
env_enablement_fn=lambda: {"token": os.getenv("VK_BOT_TOKEN", "")} if os.getenv("VK_BOT_TOKEN") else None,
|
|
99
|
+
cron_deliver_env_var="VK_HOME_CHANNEL",
|
|
100
|
+
allowed_users_env="VK_ALLOWED_USERS",
|
|
101
|
+
allow_all_env="VK_ALLOW_ALL_USERS",
|
|
102
|
+
emoji="💬",
|
|
103
|
+
pii_safe=False,
|
|
104
|
+
platform_hint="You are chatting in VK. Keep replies concise and plain-text friendly.",
|
|
105
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: vk-platform
|
|
2
|
+
label: VK
|
|
3
|
+
kind: platform
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: >
|
|
6
|
+
VK gateway adapter for Calvyn Code.
|
|
7
|
+
author: Zanderrr
|
|
8
|
+
requires_env:
|
|
9
|
+
- name: VK_BOT_TOKEN
|
|
10
|
+
description: "VK community token for the bot"
|
|
11
|
+
prompt: "VK token"
|
|
12
|
+
password: true
|
|
13
|
+
optional_env:
|
|
14
|
+
- name: VK_ALLOWED_USERS
|
|
15
|
+
description: "Comma-separated VK user IDs allowed to talk to the bot"
|
|
16
|
+
prompt: "Allowed users"
|
|
17
|
+
password: false
|
|
18
|
+
- name: VK_ALLOW_ALL_USERS
|
|
19
|
+
description: "Allow any VK user to talk to the bot"
|
|
20
|
+
prompt: "Allow all users? (true/false)"
|
|
21
|
+
password: false
|
|
22
|
+
- name: VK_HOME_CHANNEL
|
|
23
|
+
description: "Chat ID used for cron delivery"
|
|
24
|
+
prompt: "VK home channel"
|
|
25
|
+
password: false
|
package/pyproject.toml
CHANGED
|
@@ -81,7 +81,7 @@ daytona = ["daytona==0.155.0"]
|
|
|
81
81
|
vercel = ["vercel==0.5.7"]
|
|
82
82
|
hindsight = ["hindsight-client==0.6.1"]
|
|
83
83
|
dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "pytest-xdist==3.8.0", "pytest-split==0.11.0", "mcp==1.26.0", "ty==0.0.21", "ruff==0.15.10"]
|
|
84
|
-
messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.13.3", "brotlicffi==1.2.0.1", "slack-bolt==1.27.0", "slack-sdk==3.40.1", "qrcode==7.4.2"]
|
|
84
|
+
messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.13.3", "brotlicffi==1.2.0.1", "slack-bolt==1.27.0", "slack-sdk==3.40.1", "qrcode==7.4.2", "vkbottle==4.6.2"]
|
|
85
85
|
cron = [] # croniter is now a core dependency; this extra kept for back-compat
|
|
86
86
|
slack = ["slack-bolt==1.27.0", "slack-sdk==3.40.1", "aiohttp==3.13.3"]
|
|
87
87
|
matrix = ["mautrix[encryption]==0.21.0", "Markdown==3.10.2", "aiosqlite==0.22.1", "asyncpg==0.31.0", "aiohttp-socks==0.11.0"]
|
package/tools/lazy_deps.py
CHANGED
|
@@ -139,10 +139,13 @@ LAZY_DEPS: dict[str, tuple[str, ...]] = {
|
|
|
139
139
|
"alibabacloud-dingtalk==2.2.42",
|
|
140
140
|
"qrcode==7.4.2",
|
|
141
141
|
),
|
|
142
|
-
"platform.feishu": (
|
|
143
|
-
"lark-oapi==1.5.3",
|
|
144
|
-
"qrcode==7.4.2",
|
|
145
|
-
),
|
|
142
|
+
"platform.feishu": (
|
|
143
|
+
"lark-oapi==1.5.3",
|
|
144
|
+
"qrcode==7.4.2",
|
|
145
|
+
),
|
|
146
|
+
"platform.vk": (
|
|
147
|
+
"vkbottle==4.6.2",
|
|
148
|
+
),
|
|
146
149
|
|
|
147
150
|
# ─── Terminal backends ─────────────────────────────────────────────────
|
|
148
151
|
"terminal.modal": ("modal==1.3.4",),
|