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,245 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import traceback
5
+ from collections import deque
6
+ from dataclasses import dataclass
7
+ from datetime import UTC, datetime
8
+ from itertools import count
9
+ from threading import Lock
10
+ from typing import Any, Final
11
+
12
+ _LEVEL_ORDER: Final[dict[str, int]] = {
13
+ "DEBUG": logging.DEBUG,
14
+ "INFO": logging.INFO,
15
+ "WARNING": logging.WARNING,
16
+ "ERROR": logging.ERROR,
17
+ "CRITICAL": logging.CRITICAL,
18
+ }
19
+
20
+ LOG_RECORD_CONTEXT_KEYS: Final[tuple[str, ...]] = (
21
+ "category",
22
+ "chat_id",
23
+ "user_id",
24
+ "project",
25
+ "job_id",
26
+ )
27
+
28
+
29
+ def _level_no(level_name: str) -> int | None:
30
+ return _LEVEL_ORDER.get(level_name.upper())
31
+
32
+
33
+ def _context_from_dict(ctx: dict[str, Any] | None) -> dict[str, Any | None]:
34
+ if not ctx:
35
+ return {k: None for k in LOG_RECORD_CONTEXT_KEYS}
36
+ out: dict[str, Any | None] = {}
37
+ for k in LOG_RECORD_CONTEXT_KEYS:
38
+ out[k] = ctx.get(k)
39
+ return out
40
+
41
+
42
+ def _coerce_chat_or_user_id(value: Any) -> int | None:
43
+ if value is None:
44
+ return None
45
+ if isinstance(value, int):
46
+ return value
47
+ try:
48
+ return int(value)
49
+ except (TypeError, ValueError):
50
+ return None
51
+
52
+
53
+ def _coerce_category(value: Any) -> str | None:
54
+ if value is None:
55
+ return None
56
+ if isinstance(value, str):
57
+ return value
58
+ return str(value)
59
+
60
+
61
+ def _coerce_str(value: Any) -> str | None:
62
+ if value is None:
63
+ return None
64
+ return str(value)
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class BufferedLogLine:
69
+ id: int
70
+ created_at: str
71
+ level: str
72
+ logger: str
73
+ message: str
74
+ exception: str | None
75
+ category: str | None = None
76
+ chat_id: int | None = None
77
+ user_id: int | None = None
78
+ project: str | None = None
79
+ job_id: str | None = None
80
+
81
+ def to_dict(self) -> dict[str, Any]:
82
+ return {
83
+ "id": self.id,
84
+ "created_at": self.created_at,
85
+ "level": self.level,
86
+ "logger": self.logger,
87
+ "message": self.message,
88
+ "exception": self.exception,
89
+ "category": self.category,
90
+ "chat_id": self.chat_id,
91
+ "user_id": self.user_id,
92
+ "project": self.project,
93
+ "job_id": self.job_id,
94
+ }
95
+
96
+
97
+ class InMemoryLogBuffer:
98
+ def __init__(self, max_entries: int = 2000) -> None:
99
+ if max_entries < 1:
100
+ raise ValueError("max_entries must be >= 1")
101
+ self._max_entries = max_entries
102
+ self._lock = Lock()
103
+ self._lines: deque[BufferedLogLine] = deque(maxlen=max_entries)
104
+ self._seq = count(1)
105
+
106
+ @property
107
+ def max_entries(self) -> int:
108
+ return self._max_entries
109
+
110
+ def push(
111
+ self,
112
+ *,
113
+ level: str,
114
+ logger_name: str,
115
+ message: str,
116
+ exception: str | None,
117
+ context: dict[str, Any] | None = None,
118
+ ) -> int:
119
+ ctx = _context_from_dict(context)
120
+ line_id = 0
121
+ with self._lock:
122
+ line_id = next(self._seq)
123
+ created = datetime.now(UTC).isoformat().replace("+00:00", "Z")
124
+ self._lines.append(
125
+ BufferedLogLine(
126
+ id=line_id,
127
+ created_at=created,
128
+ level=level,
129
+ logger=logger_name,
130
+ message=message,
131
+ exception=exception,
132
+ category=_coerce_category(ctx.get("category")),
133
+ chat_id=_coerce_chat_or_user_id(ctx.get("chat_id")),
134
+ user_id=_coerce_chat_or_user_id(ctx.get("user_id")),
135
+ project=_coerce_str(ctx.get("project")),
136
+ job_id=_coerce_str(ctx.get("job_id")),
137
+ )
138
+ )
139
+ return line_id
140
+
141
+ def _snapshot(self) -> list[BufferedLogLine]:
142
+ with self._lock:
143
+ return list(self._lines)
144
+
145
+ def query(
146
+ self,
147
+ *,
148
+ limit: int,
149
+ after_id: int | None,
150
+ min_level: str | None,
151
+ q: str | None,
152
+ logger_sub: str | None,
153
+ chat_id: int | None = None,
154
+ user_id: int | None = None,
155
+ job_id: str | None = None,
156
+ project: str | None = None,
157
+ category: str | None = None,
158
+ ) -> tuple[list[dict[str, Any]], int]:
159
+ raw = self._snapshot()
160
+ max_seen = raw[-1].id if raw else 0
161
+
162
+ min_no: int | None = None
163
+ if min_level:
164
+ min_no = _level_no(min_level)
165
+ if min_no is None:
166
+ raise ValueError(f"unknown level: {min_level}")
167
+
168
+ def passes(line: BufferedLogLine) -> bool:
169
+ if after_id is not None and line.id <= after_id:
170
+ return False
171
+ if min_no is not None:
172
+ ln = _level_no(line.level)
173
+ if ln is None or ln < min_no:
174
+ return False
175
+ if logger_sub and logger_sub.lower() not in line.logger.lower():
176
+ return False
177
+ if q:
178
+ qq = q.lower()
179
+ hay = line.message.lower()
180
+ ex = (line.exception or "").lower()
181
+ if qq not in hay and qq not in ex:
182
+ return False
183
+ if chat_id is not None and line.chat_id != chat_id:
184
+ return False
185
+ if user_id is not None and line.user_id != user_id:
186
+ return False
187
+ if job_id is not None and (line.job_id is None or line.job_id != job_id):
188
+ return False
189
+ if project is not None and (line.project is None or line.project != project):
190
+ return False
191
+ if category is not None and (line.category is None or line.category != category):
192
+ return False
193
+ return True
194
+
195
+ filtered = [line for line in raw if passes(line)]
196
+ if after_id is None:
197
+ window = filtered[-limit:] if len(filtered) > limit else filtered
198
+ else:
199
+ window = filtered[:limit]
200
+
201
+ return [line.to_dict() for line in window], max_seen
202
+
203
+
204
+ class MemoryLogHandler(logging.Handler):
205
+ def __init__(self, buffer: InMemoryLogBuffer) -> None:
206
+ super().__init__(level=logging.DEBUG)
207
+ self._buffer = buffer
208
+
209
+ def emit(self, record: logging.LogRecord) -> None:
210
+ try:
211
+ exc_text: str | None = None
212
+ if record.exc_info:
213
+ exc_text = "".join(traceback.format_exception(*record.exc_info))
214
+ context: dict[str, Any] = {}
215
+ for k in LOG_RECORD_CONTEXT_KEYS:
216
+ if hasattr(record, k):
217
+ context[k] = getattr(record, k)
218
+ self._buffer.push(
219
+ level=record.levelname,
220
+ logger_name=record.name,
221
+ message=record.getMessage(),
222
+ exception=exc_text,
223
+ context=context if context else None,
224
+ )
225
+ except Exception: # pylint: disable=broad-except
226
+ self.handleError(record)
227
+
228
+
229
+ def attach_app_memory_log_handler(
230
+ buffer: InMemoryLogBuffer,
231
+ *,
232
+ app_logger_name: str = "app",
233
+ ) -> MemoryLogHandler:
234
+ # --reload 환경에서 새 InMemoryLogBuffer가 생성될 때 기존 핸들러를 새 버퍼에 연결된 것으로 교체합니다.
235
+ app_logger = logging.getLogger(app_logger_name)
236
+ for h in list(app_logger.handlers):
237
+ if getattr(h, "_remote_coder_admin_memory", False):
238
+ app_logger.removeHandler(h)
239
+ break
240
+ handler = MemoryLogHandler(buffer)
241
+ setattr(handler, "_remote_coder_admin_memory", True)
242
+ app_logger.addHandler(handler)
243
+ if app_logger.level == logging.NOTSET or app_logger.level > logging.INFO:
244
+ app_logger.setLevel(logging.INFO)
245
+ return handler
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from app.telegram.conversation import ConversationDbChatStats
4
+
5
+
6
+ def format_memory_monitor(stats: ConversationDbChatStats, project: str, chat_id: int) -> str:
7
+ size_kb = stats.db_size_bytes / 1024.0 if stats.db_size_bytes else 0.0
8
+ roles = ", ".join(f"{k}={v}" for k, v in sorted(stats.rows_by_role.items()))
9
+ lines = [
10
+ "Memory (SQLite)",
11
+ f"Project: {project}",
12
+ f"chat_id: {chat_id}",
13
+ f"DB path: {stats.db_path}",
14
+ f"DB exists: {'yes' if stats.db_exists else 'no'}",
15
+ f"DB size: {size_kb:.2f} KiB ({stats.db_size_bytes} bytes)",
16
+ f"Rows for this chat: {stats.total_rows}",
17
+ f"Rows by role: {roles or '(none)'}",
18
+ ]
19
+ return "\n".join(lines)