remote-coder 0.4.1__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 (78) hide show
  1. app/__init__.py +3 -0
  2. app/admin/__init__.py +0 -0
  3. app/admin/advanced_settings.py +88 -0
  4. app/admin/database_browser.py +301 -0
  5. app/admin/router.py +528 -0
  6. app/admin/static/i18n.js +401 -0
  7. app/admin/static/icons/advanced.svg +8 -0
  8. app/admin/static/icons/database.svg +5 -0
  9. app/admin/static/icons/download.svg +3 -0
  10. app/admin/static/icons/home.svg +4 -0
  11. app/admin/static/icons/logs.svg +3 -0
  12. app/admin/static/icons/projects.svg +5 -0
  13. app/admin/static/summary.js +73 -0
  14. app/admin/templates/admin.html +511 -0
  15. app/admin/templates/advanced.html +635 -0
  16. app/admin/templates/database.html +880 -0
  17. app/admin/templates/logs.html +686 -0
  18. app/admin/templates/projects.html +878 -0
  19. app/ai/__init__.py +0 -0
  20. app/ai/base.py +129 -0
  21. app/ai/claude.py +20 -0
  22. app/ai/codex.py +34 -0
  23. app/ai/factory.py +27 -0
  24. app/ai/gemini.py +20 -0
  25. app/ai/model_catalog.py +47 -0
  26. app/ai/usage.py +134 -0
  27. app/cli.py +238 -0
  28. app/config.py +130 -0
  29. app/git/__init__.py +0 -0
  30. app/git/ai_commit.py +88 -0
  31. app/git/branch_naming.py +21 -0
  32. app/git/commit_message.py +279 -0
  33. app/git/service.py +669 -0
  34. app/jobs/__init__.py +0 -0
  35. app/jobs/manager.py +770 -0
  36. app/jobs/schemas.py +116 -0
  37. app/jobs/store.py +334 -0
  38. app/main.py +265 -0
  39. app/models.py +20 -0
  40. app/monitoring/__init__.py +10 -0
  41. app/monitoring/code.py +161 -0
  42. app/monitoring/events.py +33 -0
  43. app/monitoring/git.py +103 -0
  44. app/monitoring/log_buffer.py +245 -0
  45. app/monitoring/memory.py +19 -0
  46. app/monitoring/model.py +598 -0
  47. app/projects/__init__.py +19 -0
  48. app/projects/registry.py +384 -0
  49. app/security/__init__.py +0 -0
  50. app/security/auth.py +19 -0
  51. app/system_startup.py +34 -0
  52. app/telegram/__init__.py +0 -0
  53. app/telegram/bot_instances.py +67 -0
  54. app/telegram/commands/__init__.py +64 -0
  55. app/telegram/commands/base.py +222 -0
  56. app/telegram/commands/branch.py +366 -0
  57. app/telegram/commands/clear_stop.py +221 -0
  58. app/telegram/commands/fix.py +219 -0
  59. app/telegram/commands/model.py +93 -0
  60. app/telegram/commands/monitor.py +185 -0
  61. app/telegram/commands/registry.py +110 -0
  62. app/telegram/commands/status.py +243 -0
  63. app/telegram/commands/system.py +201 -0
  64. app/telegram/confirmations.py +36 -0
  65. app/telegram/conversation.py +789 -0
  66. app/telegram/i18n.py +742 -0
  67. app/telegram/model_preferences.py +53 -0
  68. app/telegram/notifier.py +387 -0
  69. app/telegram/parser.py +267 -0
  70. app/telegram/webhook.py +988 -0
  71. app/telegram/webhook_registration.py +172 -0
  72. app/tunnel.py +104 -0
  73. remote_coder-0.4.1.dist-info/METADATA +520 -0
  74. remote_coder-0.4.1.dist-info/RECORD +78 -0
  75. remote_coder-0.4.1.dist-info/WHEEL +5 -0
  76. remote_coder-0.4.1.dist-info/entry_points.txt +2 -0
  77. remote_coder-0.4.1.dist-info/licenses/LICENSE +201 -0
  78. remote_coder-0.4.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import httpx
6
+
7
+ from app.monitoring.events import EventLogger
8
+ from app.projects.registry import ProjectRecord, build_public_webhook_url
9
+
10
+ if TYPE_CHECKING:
11
+ from app.config import Settings
12
+
13
+ _webhooklog = EventLogger("app.telegram.webhook_registration", "telegram.webhook")
14
+
15
+
16
+ def build_bot_command_payloads(
17
+ commands: list[dict[str, str]],
18
+ chat_ids: list[int] | None = None,
19
+ ) -> list[dict[str, object]]:
20
+ if not commands:
21
+ return []
22
+
23
+ payloads: list[dict[str, object]] = [
24
+ {"commands": commands},
25
+ {
26
+ "commands": commands,
27
+ "scope": {"type": "all_private_chats"},
28
+ },
29
+ ]
30
+ for chat_id in dict.fromkeys(chat_ids or []):
31
+ payloads.append(
32
+ {
33
+ "commands": commands,
34
+ "scope": {"type": "chat", "chat_id": chat_id},
35
+ }
36
+ )
37
+ return payloads
38
+
39
+
40
+ class TelegramWebhookRegistrar:
41
+ def __init__(
42
+ self,
43
+ public_base_url: str,
44
+ timeout_seconds: float = 10.0,
45
+ bot_commands: list[dict[str, str]] | None = None,
46
+ ) -> None:
47
+ self._public_base_url = public_base_url.strip().rstrip("/")
48
+ self._timeout_seconds = timeout_seconds
49
+ self._bot_commands = bot_commands or []
50
+
51
+ def set_bot_commands(self, bot_commands: list[dict[str, str]]) -> None:
52
+ self._bot_commands = bot_commands
53
+
54
+ def sync_project_commands(self, record: ProjectRecord) -> bool:
55
+ if not record.enabled or not self._bot_commands:
56
+ return False
57
+ token = record.bot_token.get_secret_value().strip()
58
+ return self._sync_bot_commands(record.name, token, record.allowed_chat_ids)
59
+
60
+ def sync_project(self, record: ProjectRecord) -> bool:
61
+ if not record.enabled:
62
+ return False
63
+
64
+ token = record.bot_token.get_secret_value().strip()
65
+ webhook_url = build_public_webhook_url(self._public_base_url, token)
66
+ payload: dict[str, object] = {
67
+ "url": webhook_url,
68
+ "drop_pending_updates": True,
69
+ }
70
+ secret = (
71
+ record.webhook_secret.get_secret_value().strip()
72
+ if record.webhook_secret
73
+ else ""
74
+ )
75
+ if secret:
76
+ payload["secret_token"] = secret
77
+
78
+ api_url = f"https://api.telegram.org/bot{token}/setWebhook"
79
+ try:
80
+ response = httpx.post(api_url, json=payload, timeout=self._timeout_seconds)
81
+ response.raise_for_status()
82
+ result = response.json()
83
+ except Exception as exc:
84
+ _webhooklog.warning(
85
+ "setWebhook request failed project=%s err=%s",
86
+ record.name,
87
+ exc,
88
+ project=record.name,
89
+ )
90
+ return False
91
+
92
+ if not result.get("ok"):
93
+ _webhooklog.warning(
94
+ "setWebhook rejected project=%s response=%s",
95
+ record.name,
96
+ result,
97
+ project=record.name,
98
+ )
99
+ return False
100
+
101
+ _webhooklog.info("setWebhook synced project=%s", record.name, project=record.name)
102
+ if self._bot_commands and not self._sync_bot_commands(record.name, token, record.allowed_chat_ids):
103
+ return False
104
+ return True
105
+
106
+ def _sync_bot_commands(self, project_name: str, token: str, chat_ids: list[int]) -> bool:
107
+ api_url = f"https://api.telegram.org/bot{token}/setMyCommands"
108
+ for payload in build_bot_command_payloads(self._bot_commands, chat_ids):
109
+ try:
110
+ response = httpx.post(
111
+ api_url,
112
+ json=payload,
113
+ timeout=self._timeout_seconds,
114
+ )
115
+ response.raise_for_status()
116
+ result = response.json()
117
+ except Exception as exc:
118
+ _webhooklog.warning(
119
+ "setMyCommands request failed project=%s err=%s",
120
+ project_name,
121
+ exc,
122
+ project=project_name,
123
+ )
124
+ return False
125
+
126
+ if not result.get("ok"):
127
+ _webhooklog.warning(
128
+ "setMyCommands rejected project=%s response=%s",
129
+ project_name,
130
+ result,
131
+ project=project_name,
132
+ )
133
+ return False
134
+
135
+ _webhooklog.info("setMyCommands synced project=%s", project_name, project=project_name)
136
+ return True
137
+
138
+
139
+ def register_all_enabled_projects(public_base_url: str, settings: "Settings") -> bool:
140
+ """Refresh Telegram webhook + command menu for every enabled registry project.
141
+
142
+ Shared by `remote-coder up` and `scripts/set_webhook.py`. Returns True only
143
+ when every enabled project synced successfully; missing/empty registry is a failure.
144
+ """
145
+ from app.projects.registry import ProjectRegistry, projects_config_path_for_settings
146
+ from app.telegram.commands import default_telegram_bot_commands
147
+
148
+ config_path = projects_config_path_for_settings(
149
+ settings.project_root,
150
+ settings.projects_config_path,
151
+ )
152
+ registry = ProjectRegistry(config_path)
153
+ registry.load()
154
+
155
+ enabled = [record for record in registry.list_projects() if record.enabled]
156
+ if not enabled:
157
+ _webhooklog.warning("no enabled projects to register config_path=%s", config_path)
158
+ return False
159
+
160
+ registrar = TelegramWebhookRegistrar(
161
+ public_base_url,
162
+ bot_commands=default_telegram_bot_commands(),
163
+ )
164
+ all_succeeded = True
165
+ for record in enabled:
166
+ if not record.bot_token.get_secret_value().strip():
167
+ _webhooklog.warning("empty bot_token project=%s", record.name, project=record.name)
168
+ all_succeeded = False
169
+ continue
170
+ if not registrar.sync_project(record):
171
+ all_succeeded = False
172
+ return all_succeeded
app/tunnel.py ADDED
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import subprocess
5
+ import time
6
+
7
+ import httpx
8
+
9
+ from app.monitoring.events import EventLogger
10
+
11
+ _tunnellog = EventLogger("app.tunnel", "tunnel")
12
+
13
+ NGROK_LOCAL_API = "http://127.0.0.1:4040/api/tunnels"
14
+
15
+
16
+ class TunnelError(RuntimeError):
17
+ pass
18
+
19
+
20
+ def ensure_ngrok_available() -> str:
21
+ path = shutil.which("ngrok")
22
+ if path is None:
23
+ raise TunnelError(
24
+ "ngrok 실행 파일을 찾을 수 없습니다. https://ngrok.com/download 에서 설치하세요."
25
+ )
26
+ return path
27
+
28
+
29
+ def ensure_ngrok_configured() -> None:
30
+ try:
31
+ result = subprocess.run(
32
+ ["ngrok", "config", "check"],
33
+ capture_output=True,
34
+ text=True,
35
+ timeout=15,
36
+ )
37
+ except (subprocess.SubprocessError, OSError) as exc:
38
+ raise TunnelError(f"ngrok 설정 확인에 실패했습니다: {exc}") from exc
39
+
40
+ combined = f"{result.stdout}\n{result.stderr}"
41
+ if "Valid configuration" not in combined:
42
+ raise TunnelError(
43
+ "ngrok AuthToken이 설정되지 않았습니다. https://dashboard.ngrok.com 에서 토큰을 발급받아 "
44
+ "`ngrok config add-authtoken <token>` 을 실행하세요."
45
+ )
46
+
47
+
48
+ def fetch_public_url() -> str | None:
49
+ try:
50
+ response = httpx.get(NGROK_LOCAL_API, timeout=2.0)
51
+ response.raise_for_status()
52
+ data = response.json()
53
+ except (httpx.HTTPError, ValueError):
54
+ return None
55
+ for tunnel in data.get("tunnels", []):
56
+ public_url = tunnel.get("public_url", "")
57
+ if public_url.startswith("https"):
58
+ return public_url
59
+ return None
60
+
61
+
62
+ class NgrokTunnel:
63
+ def __init__(self, port: int = 8000) -> None:
64
+ self._port = port
65
+ self._process: subprocess.Popen | None = None
66
+ self.public_url: str | None = None
67
+
68
+ def start(self, *, startup_timeout: float = 15.0) -> str:
69
+ ensure_ngrok_available()
70
+ ensure_ngrok_configured()
71
+ self._process = subprocess.Popen(
72
+ ["ngrok", "http", str(self._port)],
73
+ stdout=subprocess.DEVNULL,
74
+ stderr=subprocess.DEVNULL,
75
+ )
76
+ url = self._wait_for_public_url(startup_timeout)
77
+ if url is None:
78
+ self.stop()
79
+ raise TunnelError(
80
+ "ngrok 공개 URL을 가져오지 못했습니다. 다른 ngrok 세션이 실행 중인지 확인하세요."
81
+ )
82
+ self.public_url = url
83
+ _tunnellog.info("ngrok tunnel started url=%s port=%d", url, self._port)
84
+ return url
85
+
86
+ def _wait_for_public_url(self, timeout: float) -> str | None:
87
+ deadline = time.monotonic() + timeout
88
+ while time.monotonic() < deadline:
89
+ url = fetch_public_url()
90
+ if url:
91
+ return url
92
+ time.sleep(0.5)
93
+ return None
94
+
95
+ def stop(self) -> None:
96
+ if self._process is None:
97
+ return
98
+ self._process.terminate()
99
+ try:
100
+ self._process.wait(timeout=5)
101
+ except subprocess.TimeoutExpired:
102
+ self._process.kill()
103
+ self._process = None
104
+ _tunnellog.info("ngrok tunnel stopped")