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,201 @@
1
+ from __future__ import annotations
2
+
3
+ from app.telegram.commands.base import (
4
+ HELP_AGENT_TOPIC,
5
+ HELP_ASK_TOPIC,
6
+ HELP_PLAN_TOPIC,
7
+ HELP_TEXT,
8
+ CommandContext,
9
+ InlineButton,
10
+ TelegramCommand,
11
+ TelegramMessage,
12
+ _button_rows,
13
+ _cmd_evt,
14
+ effective_model_for_chat,
15
+ effective_project_name_for_chat,
16
+ format_usage,
17
+ )
18
+
19
+
20
+ class StartCommand(TelegramCommand):
21
+ name = "/start"
22
+ description = "Show the menu and project status"
23
+
24
+ _TOPIC_TEXT: dict[str, str] = {
25
+ "manage": "Choose a command.",
26
+ "modes": "Choose a mode guide.",
27
+ }
28
+
29
+ def execute(self, message: TelegramMessage, ctx: CommandContext) -> str:
30
+ tokens = message.text.strip().split()
31
+ if len(tokens) == 2:
32
+ topic = tokens[1].lower()
33
+ topic_text = self._TOPIC_TEXT.get(topic)
34
+ if topic_text is not None:
35
+ return topic_text
36
+ project_name = effective_project_name_for_chat(ctx, message.chat_id)
37
+ if not project_name:
38
+ return "✅ Remote AI Coder is ready.\n\nWelcome to Remote AI Coder."
39
+ entry = ctx.project_registry.get(project_name)
40
+ if not entry:
41
+ return (
42
+ "✅ Remote AI Coder is ready.\n\n"
43
+ "Welcome to Remote AI Coder.\n"
44
+ f"- Project: {project_name} (not registered)"
45
+ )
46
+ try:
47
+ current_branch = ctx.git_service.get_current_branch(entry.root_path)
48
+ except RuntimeError:
49
+ current_branch = "(check failed)"
50
+ state = "enabled" if entry.enabled else "disabled"
51
+ return "\n".join(
52
+ [
53
+ "✅ Remote AI Coder is ready.",
54
+ "",
55
+ "Welcome to Remote AI Coder.",
56
+ f"- Project: {entry.name}",
57
+ f"- root_path: {entry.root_path}",
58
+ f"- default_model: {entry.default_model.value}",
59
+ f"- current_branch: {current_branch}",
60
+ f"- worktree_base_dir: {entry.worktree_base_dir}",
61
+ f"- enabled: {state}",
62
+ ]
63
+ )
64
+
65
+ def get_inline_buttons(
66
+ self,
67
+ message: TelegramMessage | None = None,
68
+ ctx: CommandContext | None = None,
69
+ ) -> list[list[InlineButton]] | None:
70
+ tokens = message.text.strip().split() if message is not None else []
71
+ topic = tokens[1].lower() if len(tokens) == 2 else ""
72
+
73
+ if topic == "manage":
74
+ return [
75
+ [
76
+ InlineButton("Branch", "/branch"),
77
+ InlineButton("Pull", "/pull"),
78
+ ],
79
+ [
80
+ InlineButton("Rebase", "/rebase"),
81
+ InlineButton("Open PR", "/pr"),
82
+ ],
83
+ [
84
+ InlineButton("Stop", "/stop"),
85
+ InlineButton("Status", "/status"),
86
+ ],
87
+ [
88
+ InlineButton("Model", "/model"),
89
+ InlineButton("Reset", "/init"),
90
+ ],
91
+ [
92
+ InlineButton("Back", "/start"),
93
+ ],
94
+ ]
95
+ if topic == "modes":
96
+ return [
97
+ [
98
+ InlineButton("AGENTS mode", "/help agent"),
99
+ InlineButton("PLAN mode", "/help plan"),
100
+ InlineButton("ASK mode", "/help ask"),
101
+ ],
102
+ [InlineButton("Back", "/start")],
103
+ ]
104
+ return [
105
+ [InlineButton("Help", "/help"), InlineButton("Modes", "/start modes")],
106
+ [InlineButton("Monitor", "/monitor"), InlineButton("Clean", "/clear")],
107
+ [InlineButton("Manage", "/start manage"), InlineButton("Reports", "/reports")],
108
+ ]
109
+
110
+
111
+ class HelpCommand(TelegramCommand):
112
+ name = "/help"
113
+ description = "Show available commands"
114
+ _registry: dict[str, TelegramCommand] | None = None
115
+
116
+ def execute(self, message: TelegramMessage, ctx: CommandContext) -> str:
117
+ tokens = message.text.strip().split()
118
+ if len(tokens) >= 2:
119
+ raw = tokens[1]
120
+ topic_aliases = {"에이전트": "agent", "계획": "plan", "질문": "ask"}
121
+ topic = topic_aliases.get(raw, raw.lower())
122
+ if topic in ("agent", "agents"):
123
+ return HELP_AGENT_TOPIC
124
+ if topic == "plan":
125
+ return HELP_PLAN_TOPIC
126
+ if topic == "ask":
127
+ return HELP_ASK_TOPIC
128
+ if len(tokens) >= 2 and self._registry is not None:
129
+ subcmd = self._registry.get("/" + tokens[1])
130
+ if subcmd is not None and subcmd.menu_text:
131
+ return subcmd.menu_text
132
+ return HELP_TEXT
133
+
134
+ def get_inline_buttons(
135
+ self,
136
+ message: TelegramMessage | None = None,
137
+ ctx: CommandContext | None = None,
138
+ ) -> list[list[InlineButton]] | None:
139
+ if self._registry is None:
140
+ return None
141
+ tokens = message.text.strip().split() if message else []
142
+ if len(tokens) >= 2:
143
+ topic = tokens[1].lower()
144
+ if topic in ("agent", "agents", "plan", "ask"):
145
+ return [[InlineButton("← Back", "/help")]]
146
+ subcmd = self._registry.get("/" + tokens[1])
147
+ if subcmd is not None:
148
+ sub_buttons = subcmd.get_inline_buttons(None, ctx) or []
149
+ return sub_buttons + [[InlineButton("← Back", "/help")]]
150
+ menu_cmds = [
151
+ cmd for name, cmd in self._registry.items()
152
+ if name not in ("/help", "/start") and cmd.menu_text
153
+ ]
154
+ if not menu_cmds:
155
+ return None
156
+ buttons = [InlineButton(cmd.name[1:], f"/help {cmd.name[1:]}") for cmd in menu_cmds]
157
+ return _button_rows(buttons, per_row=2)
158
+
159
+
160
+ class InitCommand(TelegramCommand):
161
+ name = "/init"
162
+ description = "Reset model settings and pending confirmations"
163
+
164
+ def execute(self, message: TelegramMessage, ctx: CommandContext) -> str:
165
+ tokens = message.text.strip().split()
166
+ if len(tokens) != 1:
167
+ return format_usage("/init")
168
+
169
+ chat_id = message.chat_id
170
+ project_name = effective_project_name_for_chat(ctx, chat_id)
171
+ ctx.model_preferences.clear(project_name, chat_id)
172
+ ctx.confirmation_store.pop(project_name, chat_id)
173
+ _cmd_evt.info("init reset", chat_id=chat_id)
174
+
175
+ project_name = effective_project_name_for_chat(ctx, chat_id)
176
+ if not project_name:
177
+ return (
178
+ "This chat's default model and pending confirmation were reset.\n"
179
+ "No project context is configured."
180
+ )
181
+
182
+ entry = ctx.project_registry.get(project_name)
183
+ if not entry:
184
+ return (
185
+ "This chat's default model and pending confirmation were reset.\n"
186
+ f"Project `{project_name}` was not found. "
187
+ "Check the project settings in the admin UI."
188
+ )
189
+ if not entry.enabled:
190
+ return (
191
+ "This chat's default model and pending confirmation were reset.\n"
192
+ f"Project `{project_name}` is disabled. "
193
+ "Check the enabled state in the admin UI."
194
+ )
195
+
196
+ model = effective_model_for_chat(ctx, chat_id, project_name)
197
+ return (
198
+ "This chat's default model and pending confirmation were reset.\n"
199
+ f"Project: {project_name}\n"
200
+ f"Default model: {model.value}"
201
+ )
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from threading import Lock
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from app.jobs.schemas import JobRequest
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class PendingConfirmation:
13
+ command_name: str
14
+ action: str
15
+ job_request: JobRequest | None = None
16
+ original_text: str | None = None
17
+ target_job_id: str | None = None
18
+ prepared_payload: str | None = None
19
+
20
+
21
+ class InMemoryConfirmationStore:
22
+ def __init__(self) -> None:
23
+ self._values: dict[tuple[str | None, int], PendingConfirmation] = {}
24
+ self._lock = Lock()
25
+
26
+ def get(self, project_name: str | None, chat_id: int) -> PendingConfirmation | None:
27
+ with self._lock:
28
+ return self._values.get((project_name, chat_id))
29
+
30
+ def set(self, project_name: str | None, chat_id: int, confirmation: PendingConfirmation) -> None:
31
+ with self._lock:
32
+ self._values[(project_name, chat_id)] = confirmation
33
+
34
+ def pop(self, project_name: str | None, chat_id: int) -> PendingConfirmation | None:
35
+ with self._lock:
36
+ return self._values.pop((project_name, chat_id), None)