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.
- app/__init__.py +3 -0
- app/admin/__init__.py +0 -0
- app/admin/advanced_settings.py +88 -0
- app/admin/database_browser.py +301 -0
- app/admin/router.py +528 -0
- app/admin/static/i18n.js +401 -0
- app/admin/static/icons/advanced.svg +8 -0
- app/admin/static/icons/database.svg +5 -0
- app/admin/static/icons/download.svg +3 -0
- app/admin/static/icons/home.svg +4 -0
- app/admin/static/icons/logs.svg +3 -0
- app/admin/static/icons/projects.svg +5 -0
- app/admin/static/summary.js +73 -0
- app/admin/templates/admin.html +511 -0
- app/admin/templates/advanced.html +635 -0
- app/admin/templates/database.html +880 -0
- app/admin/templates/logs.html +686 -0
- app/admin/templates/projects.html +878 -0
- app/ai/__init__.py +0 -0
- app/ai/base.py +129 -0
- app/ai/claude.py +20 -0
- app/ai/codex.py +34 -0
- app/ai/factory.py +27 -0
- app/ai/gemini.py +20 -0
- app/ai/model_catalog.py +47 -0
- app/ai/usage.py +134 -0
- app/cli.py +238 -0
- app/config.py +130 -0
- app/git/__init__.py +0 -0
- app/git/ai_commit.py +88 -0
- app/git/branch_naming.py +21 -0
- app/git/commit_message.py +279 -0
- app/git/service.py +669 -0
- app/jobs/__init__.py +0 -0
- app/jobs/manager.py +770 -0
- app/jobs/schemas.py +116 -0
- app/jobs/store.py +334 -0
- app/main.py +265 -0
- app/models.py +20 -0
- app/monitoring/__init__.py +10 -0
- app/monitoring/code.py +161 -0
- app/monitoring/events.py +33 -0
- app/monitoring/git.py +103 -0
- app/monitoring/log_buffer.py +245 -0
- app/monitoring/memory.py +19 -0
- app/monitoring/model.py +598 -0
- app/projects/__init__.py +19 -0
- app/projects/registry.py +384 -0
- app/security/__init__.py +0 -0
- app/security/auth.py +19 -0
- app/system_startup.py +34 -0
- app/telegram/__init__.py +0 -0
- app/telegram/bot_instances.py +67 -0
- app/telegram/commands/__init__.py +64 -0
- app/telegram/commands/base.py +222 -0
- app/telegram/commands/branch.py +366 -0
- app/telegram/commands/clear_stop.py +221 -0
- app/telegram/commands/fix.py +219 -0
- app/telegram/commands/model.py +93 -0
- app/telegram/commands/monitor.py +185 -0
- app/telegram/commands/registry.py +110 -0
- app/telegram/commands/status.py +243 -0
- app/telegram/commands/system.py +201 -0
- app/telegram/confirmations.py +36 -0
- app/telegram/conversation.py +789 -0
- app/telegram/i18n.py +742 -0
- app/telegram/model_preferences.py +53 -0
- app/telegram/notifier.py +387 -0
- app/telegram/parser.py +267 -0
- app/telegram/webhook.py +988 -0
- app/telegram/webhook_registration.py +172 -0
- app/tunnel.py +104 -0
- remote_coder-0.4.1.dist-info/METADATA +520 -0
- remote_coder-0.4.1.dist-info/RECORD +78 -0
- remote_coder-0.4.1.dist-info/WHEEL +5 -0
- remote_coder-0.4.1.dist-info/entry_points.txt +2 -0
- remote_coder-0.4.1.dist-info/licenses/LICENSE +201 -0
- 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
|
app/monitoring/memory.py
ADDED
|
@@ -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)
|