remote-coder 0.4.3__tar.gz → 0.4.4__tar.gz
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.
- {remote_coder-0.4.3 → remote_coder-0.4.4}/PKG-INFO +1 -1
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/__init__.py +1 -1
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/cli.py +15 -12
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/jobs/manager.py +82 -99
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/jobs/schemas.py +0 -1
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/monitoring/model.py +58 -1
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/commands/__init__.py +0 -2
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/commands/base.py +19 -2
- remote_coder-0.4.4/app/telegram/commands/fix.py +19 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/commands/registry.py +1 -1
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/commands/system.py +14 -5
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/confirmations.py +1 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/i18n.py +35 -6
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/parser.py +44 -11
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/webhook.py +208 -83
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/tunnel.py +5 -5
- {remote_coder-0.4.3 → remote_coder-0.4.4}/pyproject.toml +1 -1
- {remote_coder-0.4.3 → remote_coder-0.4.4}/remote_coder.egg-info/PKG-INFO +1 -1
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_cli.py +1 -1
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_command_parser.py +26 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_commands.py +18 -219
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_commit_message_formatter.py +2 -3
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_job_manager.py +26 -41
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_monitoring.py +45 -2
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_webhook.py +286 -1
- remote_coder-0.4.3/app/telegram/commands/fix.py +0 -219
- {remote_coder-0.4.3 → remote_coder-0.4.4}/LICENSE +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/README.md +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/__init__.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/advanced_settings.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/database_browser.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/router.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/static/i18n.js +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/static/icons/advanced.svg +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/static/icons/database.svg +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/static/icons/download.svg +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/static/icons/home.svg +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/static/icons/logs.svg +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/static/icons/projects.svg +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/static/summary.js +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/templates/admin.html +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/templates/advanced.html +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/templates/database.html +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/templates/logs.html +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/admin/templates/projects.html +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/ai/__init__.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/ai/base.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/ai/claude.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/ai/codex.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/ai/factory.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/ai/gemini.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/ai/model_catalog.py +1 -1
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/ai/usage.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/config.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/diagnostics.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/git/__init__.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/git/ai_commit.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/git/branch_naming.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/git/commit_message.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/git/service.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/jobs/__init__.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/jobs/store.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/main.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/models.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/monitoring/__init__.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/monitoring/code.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/monitoring/events.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/monitoring/git.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/monitoring/log_buffer.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/monitoring/memory.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/projects/__init__.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/projects/registry.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/security/__init__.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/security/auth.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/system_startup.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/__init__.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/bot_instances.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/commands/branch.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/commands/clear_stop.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/commands/model.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/commands/monitor.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/commands/status.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/conversation.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/model_preferences.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/notifier.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/app/telegram/webhook_registration.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/remote_coder.egg-info/SOURCES.txt +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/remote_coder.egg-info/dependency_links.txt +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/remote_coder.egg-info/entry_points.txt +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/remote_coder.egg-info/requires.txt +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/remote_coder.egg-info/top_level.txt +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/setup.cfg +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_admin_router.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_ai_base.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_ai_commit.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_ai_factory.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_auth.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_bot_instance_manager.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_branch_naming.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_claude_runner.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_codex_runner.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_config.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_conversation_store.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_database_browser.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_diagnostics.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_event_logger.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_gemini_runner.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_git_service.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_i18n.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_job_status.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_job_store.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_log_buffer.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_notifier.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_project_registry.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_project_scoped_state.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_system_startup.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_tunnel.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_webhook_multibot.py +0 -0
- {remote_coder-0.4.3 → remote_coder-0.4.4}/tests/test_webhook_registration.py +0 -0
|
@@ -55,7 +55,7 @@ def main(argv: Sequence[str] | None = None) -> None:
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def _run_server(*, host: str, port: int, reload: bool, log_level: str) -> None:
|
|
58
|
-
print("🚀
|
|
58
|
+
print("🚀 Starting server... (press Ctrl+C to stop)")
|
|
59
59
|
uvicorn.run("app.main:app", host=host, port=port, reload=reload, log_level=log_level)
|
|
60
60
|
|
|
61
61
|
|
|
@@ -70,32 +70,35 @@ def run_up(*, host: str, port: int, reload: bool, log_level: str, tunnel: bool =
|
|
|
70
70
|
|
|
71
71
|
ngrok = NgrokTunnel(port)
|
|
72
72
|
try:
|
|
73
|
-
print("🌐 ngrok
|
|
73
|
+
print("🌐 Starting ngrok tunnel...")
|
|
74
74
|
public_url = ngrok.start()
|
|
75
75
|
except TunnelError as exc:
|
|
76
76
|
print(f"❌ {exc}")
|
|
77
77
|
raise SystemExit(1) from exc
|
|
78
78
|
|
|
79
|
-
print(f"🔗
|
|
79
|
+
print(f"🔗 Public HTTPS URL: {public_url}")
|
|
80
80
|
os.environ["TELEGRAM_WEBHOOK_PUBLIC_BASE_URL"] = public_url
|
|
81
81
|
get_settings.cache_clear()
|
|
82
82
|
settings = get_settings()
|
|
83
83
|
|
|
84
|
-
print("📨 Telegram
|
|
84
|
+
print("📨 Registering Telegram webhooks and command menu...")
|
|
85
85
|
if not register_all_enabled_projects(public_url, settings):
|
|
86
86
|
if _has_enabled_projects(settings):
|
|
87
|
-
print(
|
|
87
|
+
print(
|
|
88
|
+
"⚠️ Some project webhook registrations failed. "
|
|
89
|
+
"If existing registrations are valid, the service may still work."
|
|
90
|
+
)
|
|
88
91
|
else:
|
|
89
92
|
print(
|
|
90
|
-
f"🛠️
|
|
91
|
-
"
|
|
93
|
+
f"🛠️ No projects registered yet. Open http://127.0.0.1:{port}/ in a browser and "
|
|
94
|
+
"register your first project (bot token, repository, allowed Chat IDs)."
|
|
92
95
|
)
|
|
93
96
|
|
|
94
97
|
try:
|
|
95
98
|
_run_server(host=host, port=port, reload=reload, log_level=log_level)
|
|
96
99
|
finally:
|
|
97
100
|
ngrok.stop()
|
|
98
|
-
print("🛑 ngrok
|
|
101
|
+
print("🛑 ngrok tunnel stopped.")
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
def _has_enabled_projects(settings) -> bool:
|
|
@@ -111,9 +114,9 @@ def run_doctor() -> None:
|
|
|
111
114
|
from app.diagnostics import check_prerequisites
|
|
112
115
|
|
|
113
116
|
report = check_prerequisites()
|
|
114
|
-
print("
|
|
117
|
+
print("Prerequisite checks:")
|
|
115
118
|
if report.ngrok_ok:
|
|
116
|
-
print(" ✅ ngrok:
|
|
119
|
+
print(" ✅ ngrok: installed and AuthToken configured")
|
|
117
120
|
else:
|
|
118
121
|
print(f" ⚠️ ngrok: {report.ngrok_detail}")
|
|
119
122
|
|
|
@@ -122,8 +125,8 @@ def run_doctor() -> None:
|
|
|
122
125
|
print(f" ✅ AI CLI: {', '.join(installed)}")
|
|
123
126
|
else:
|
|
124
127
|
print(
|
|
125
|
-
" ⚠️ AI CLI(claude/codex/gemini)
|
|
126
|
-
"(
|
|
128
|
+
" ⚠️ AI CLI (claude/codex/gemini) not found. Install at least one. "
|
|
129
|
+
"(e.g. npm install -g @anthropic-ai/claude-code)"
|
|
127
130
|
)
|
|
128
131
|
|
|
129
132
|
|
|
@@ -460,22 +460,24 @@ class JobManager:
|
|
|
460
460
|
if self.is_fix_candidate(job, project, chat_id)
|
|
461
461
|
][:limit]
|
|
462
462
|
|
|
463
|
-
def
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
463
|
+
def resolve_fix_target_job(self, job_id: str, project: str, chat_id: int) -> Job | None:
|
|
464
|
+
job = self._job_store.get(job_id)
|
|
465
|
+
if job is None:
|
|
466
|
+
return None
|
|
467
|
+
visited: set[str] = set()
|
|
468
|
+
while job is not None and job.id not in visited:
|
|
469
|
+
visited.add(job.id)
|
|
470
|
+
if self.is_fix_candidate(job, project, chat_id):
|
|
471
|
+
if job.request.mode is JobMode.AGENT_FIX and job.request.parent_job_id:
|
|
472
|
+
parent = self._job_store.get(job.request.parent_job_id)
|
|
473
|
+
if parent is not None and self.is_fix_candidate(parent, project, chat_id):
|
|
474
|
+
return parent
|
|
475
|
+
return job
|
|
476
|
+
if job.request.parent_job_id:
|
|
477
|
+
job = self._job_store.get(job.request.parent_job_id)
|
|
478
|
+
else:
|
|
479
|
+
break
|
|
480
|
+
return None
|
|
479
481
|
|
|
480
482
|
@staticmethod
|
|
481
483
|
def compose_fix_source_prompt(parent_job: Job, fix_instruction: str) -> str:
|
|
@@ -495,15 +497,11 @@ class JobManager:
|
|
|
495
497
|
"Do not add new files or unrelated changes."
|
|
496
498
|
)
|
|
497
499
|
|
|
498
|
-
def execute_fix_job(
|
|
499
|
-
self,
|
|
500
|
-
request: JobRequest,
|
|
501
|
-
prepared_message: str | None = None,
|
|
502
|
-
) -> Job:
|
|
500
|
+
def execute_fix_job(self, request: JobRequest) -> Job:
|
|
503
501
|
if request.mode is not JobMode.AGENT_FIX:
|
|
504
502
|
raise ValueError("execute_fix_job requires JobMode.AGENT_FIX")
|
|
505
|
-
if request.fix_kind is
|
|
506
|
-
raise ValueError("execute_fix_job requires
|
|
503
|
+
if request.fix_kind is not FixKind.SOURCE:
|
|
504
|
+
raise ValueError("execute_fix_job requires FixKind.SOURCE")
|
|
507
505
|
if not request.parent_job_id:
|
|
508
506
|
raise ValueError("execute_fix_job requires parent_job_id")
|
|
509
507
|
|
|
@@ -521,9 +519,9 @@ class JobManager:
|
|
|
521
519
|
if accepted_message_id is not None:
|
|
522
520
|
job.accepted_message_id = accepted_message_id
|
|
523
521
|
self._job_store.update(job)
|
|
524
|
-
return self._run_fix(job.id
|
|
522
|
+
return self._run_fix(job.id)
|
|
525
523
|
|
|
526
|
-
def _run_fix(self, job_id: str
|
|
524
|
+
def _run_fix(self, job_id: str) -> Job:
|
|
527
525
|
job = self._job_store.get(job_id)
|
|
528
526
|
if job is None:
|
|
529
527
|
_joblog.warning("run_fix requested for missing job job_id=%s", job_id)
|
|
@@ -553,10 +551,12 @@ class JobManager:
|
|
|
553
551
|
self._job_store.update(job)
|
|
554
552
|
|
|
555
553
|
failed_stage = "fix_resolve_target"
|
|
556
|
-
parent_job = self.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
554
|
+
parent_job = self.resolve_fix_target_job(
|
|
555
|
+
job.request.parent_job_id or "",
|
|
556
|
+
job.request.project,
|
|
557
|
+
job.request.chat_id,
|
|
558
|
+
)
|
|
559
|
+
if parent_job is None:
|
|
560
560
|
raise RuntimeError("Fix target job was not found or can no longer be fixed.")
|
|
561
561
|
assert parent_job.branch is not None
|
|
562
562
|
assert parent_job.commit_hash is not None
|
|
@@ -577,90 +577,73 @@ class JobManager:
|
|
|
577
577
|
created_worktree_for_job = True
|
|
578
578
|
self._git_service.ensure_worktree_writable(worktree_path)
|
|
579
579
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
mode=JobMode.AGENT,
|
|
594
|
-
)
|
|
580
|
+
failed_stage = "fix_runner"
|
|
581
|
+
runner = self._runner_factory.create(job.request.model)
|
|
582
|
+
timeout_seconds = self._effective_job_timeout_seconds()
|
|
583
|
+
fix_prompt = self.compose_fix_source_prompt(parent_job, job.request.instruction)
|
|
584
|
+
runner_result = runner.run(
|
|
585
|
+
RunnerInput(
|
|
586
|
+
instruction=fix_prompt,
|
|
587
|
+
cwd=worktree_path,
|
|
588
|
+
timeout_seconds=timeout_seconds,
|
|
589
|
+
model_id=job.request.model_id,
|
|
590
|
+
env=None,
|
|
591
|
+
cancel_event=cancel_event,
|
|
592
|
+
mode=JobMode.AGENT,
|
|
595
593
|
)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
failed_stage = "fix_collect_changes"
|
|
601
|
-
new_changed = self._git_service.collect_changes(worktree_path)
|
|
602
|
-
merged = list(dict.fromkeys([*parent_job.changed_files, *new_changed]))
|
|
603
|
-
job.changed_files = merged
|
|
604
|
-
|
|
605
|
-
if not new_changed:
|
|
606
|
-
job.branch = parent_job.branch
|
|
607
|
-
job.commit_hash = parent_job.commit_hash
|
|
608
|
-
job.mark_succeeded()
|
|
609
|
-
self._job_store.update(job)
|
|
610
|
-
_joblog.info(
|
|
611
|
-
"fix source produced no changes parent=%s",
|
|
612
|
-
parent_job.id,
|
|
613
|
-
**self._job_ctx(job),
|
|
614
|
-
)
|
|
615
|
-
else:
|
|
616
|
-
failed_stage = "fix_message"
|
|
617
|
-
ai_title = None
|
|
618
|
-
ai_body = None
|
|
619
|
-
if self._ai_commit_body_generator is not None:
|
|
620
|
-
ai_title, ai_body = self._ai_commit_body_generator.generate(
|
|
621
|
-
instruction=self.compose_fix_source_prompt(
|
|
622
|
-
parent_job, job.request.instruction
|
|
623
|
-
),
|
|
624
|
-
changed_files=merged,
|
|
625
|
-
model_name=job.request.model,
|
|
626
|
-
)
|
|
627
|
-
commit_message = CommitMessageFormatter.format(
|
|
628
|
-
job_id=parent_job.id,
|
|
629
|
-
instruction=parent_job.request.instruction,
|
|
630
|
-
changed_files=merged,
|
|
631
|
-
ai_body=ai_body,
|
|
632
|
-
ai_title=ai_title,
|
|
633
|
-
)
|
|
594
|
+
)
|
|
595
|
+
self._save_runner_log(job, runner_result, worktree_base)
|
|
596
|
+
if runner_result.exit_code != 0:
|
|
597
|
+
raise RuntimeError(runner_result.stderr.strip() or "runner failed")
|
|
634
598
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
599
|
+
failed_stage = "fix_collect_changes"
|
|
600
|
+
new_changed = self._git_service.collect_changes(worktree_path)
|
|
601
|
+
merged = list(dict.fromkeys([*parent_job.changed_files, *new_changed]))
|
|
602
|
+
job.changed_files = merged
|
|
638
603
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
604
|
+
if not new_changed:
|
|
605
|
+
job.branch = parent_job.branch
|
|
606
|
+
job.commit_hash = parent_job.commit_hash
|
|
607
|
+
job.mark_succeeded()
|
|
608
|
+
self._job_store.update(job)
|
|
609
|
+
_joblog.info(
|
|
610
|
+
"fix source produced no changes parent=%s",
|
|
611
|
+
parent_job.id,
|
|
612
|
+
**self._job_ctx(job),
|
|
613
|
+
)
|
|
614
|
+
else:
|
|
615
|
+
failed_stage = "fix_message"
|
|
616
|
+
ai_title = None
|
|
617
|
+
ai_body = None
|
|
618
|
+
if self._ai_commit_body_generator is not None:
|
|
619
|
+
ai_title, ai_body = self._ai_commit_body_generator.generate(
|
|
620
|
+
instruction=self.compose_fix_source_prompt(
|
|
621
|
+
parent_job, job.request.instruction
|
|
622
|
+
),
|
|
623
|
+
changed_files=merged,
|
|
624
|
+
model_name=job.request.model,
|
|
642
625
|
)
|
|
626
|
+
commit_message = CommitMessageFormatter.format(
|
|
627
|
+
job_id=parent_job.id,
|
|
628
|
+
instruction=parent_job.request.instruction,
|
|
629
|
+
changed_files=merged,
|
|
630
|
+
ai_body=ai_body,
|
|
631
|
+
ai_title=ai_title,
|
|
632
|
+
)
|
|
643
633
|
|
|
644
|
-
parent_job.commit_hash = job.commit_hash
|
|
645
|
-
parent_job.changed_files = merged
|
|
646
|
-
self._job_store.update(parent_job)
|
|
647
|
-
|
|
648
|
-
job.mark_succeeded()
|
|
649
|
-
self._job_store.update(job)
|
|
650
|
-
else:
|
|
651
634
|
failed_stage = "fix_amend"
|
|
652
|
-
|
|
653
|
-
prepared_message = self.build_fix_commit_preview(parent_job)
|
|
654
|
-
job.changed_files = list(parent_job.changed_files)
|
|
655
|
-
job.commit_hash = self._git_service.amend_commit(worktree_path, prepared_message)
|
|
635
|
+
job.commit_hash = self._git_service.amend_commit(worktree_path, commit_message)
|
|
656
636
|
job.branch = parent_job.branch
|
|
657
637
|
|
|
658
638
|
failed_stage = "fix_push"
|
|
659
639
|
self._git_service.push_branch_force_with_lease(
|
|
660
640
|
project_path, remote, parent_job.branch
|
|
661
641
|
)
|
|
642
|
+
|
|
662
643
|
parent_job.commit_hash = job.commit_hash
|
|
644
|
+
parent_job.changed_files = merged
|
|
663
645
|
self._job_store.update(parent_job)
|
|
646
|
+
|
|
664
647
|
job.mark_succeeded()
|
|
665
648
|
self._job_store.update(job)
|
|
666
649
|
except Exception as exc: # pylint: disable=broad-except
|
|
@@ -234,6 +234,7 @@ def _normalize_token_dict(raw: object) -> dict[str, int]:
|
|
|
234
234
|
"output_tokens": "output",
|
|
235
235
|
"reasoning_output_tokens": "reasoning",
|
|
236
236
|
"total_tokens": "total",
|
|
237
|
+
"candidates": "output",
|
|
237
238
|
"input": "input",
|
|
238
239
|
"output": "output",
|
|
239
240
|
"cached": "cached",
|
|
@@ -553,7 +554,7 @@ class GeminiUsageProvider(ModelUsageProvider):
|
|
|
553
554
|
else:
|
|
554
555
|
lines.append(f"Version check failed (exit {proc.returncode}).")
|
|
555
556
|
|
|
556
|
-
lines.extend(self.
|
|
557
|
+
lines.extend(self._model_probe(timeout_seconds))
|
|
557
558
|
return "\n".join(lines)
|
|
558
559
|
|
|
559
560
|
@staticmethod
|
|
@@ -563,6 +564,49 @@ class GeminiUsageProvider(ModelUsageProvider):
|
|
|
563
564
|
"Install: npm install -g @google/gemini-cli",
|
|
564
565
|
]
|
|
565
566
|
|
|
567
|
+
@staticmethod
|
|
568
|
+
def _model_probe(timeout_seconds: int) -> list[str]:
|
|
569
|
+
lines = ["", "Live model probe (--output-format json):"]
|
|
570
|
+
try:
|
|
571
|
+
proc = subprocess.run(
|
|
572
|
+
[
|
|
573
|
+
"gemini",
|
|
574
|
+
"--skip-trust",
|
|
575
|
+
"--output-format",
|
|
576
|
+
"json",
|
|
577
|
+
"-p",
|
|
578
|
+
"Reply with ok only.",
|
|
579
|
+
],
|
|
580
|
+
capture_output=True,
|
|
581
|
+
text=True,
|
|
582
|
+
timeout=timeout_seconds,
|
|
583
|
+
shell=False,
|
|
584
|
+
)
|
|
585
|
+
except subprocess.TimeoutExpired:
|
|
586
|
+
return [*lines, "- Probe timed out before Gemini returned JSON stats."]
|
|
587
|
+
except FileNotFoundError:
|
|
588
|
+
return [*lines, "- Probe skipped because `gemini` is not available on PATH."]
|
|
589
|
+
|
|
590
|
+
raw = (proc.stdout or "").strip()
|
|
591
|
+
if proc.returncode != 0:
|
|
592
|
+
message = (proc.stderr or raw or "no output").strip()
|
|
593
|
+
return [*lines, f"- Probe failed (exit {proc.returncode}): {message[:400]}"]
|
|
594
|
+
try:
|
|
595
|
+
data = json.loads(raw)
|
|
596
|
+
except json.JSONDecodeError:
|
|
597
|
+
return [*lines, "- Probe returned non-JSON output.", raw[:400]]
|
|
598
|
+
|
|
599
|
+
model, tokens = _gemini_model_stats(data)
|
|
600
|
+
if model:
|
|
601
|
+
lines.append(f"- Observed detailed model: {model}")
|
|
602
|
+
if tokens:
|
|
603
|
+
formatted = format_token_usage(tokens)
|
|
604
|
+
if formatted:
|
|
605
|
+
lines.append(f"- Observed tokens: {formatted}")
|
|
606
|
+
if len(lines) == 2:
|
|
607
|
+
lines.append("- No model stats found in Gemini JSON output.")
|
|
608
|
+
return lines
|
|
609
|
+
|
|
566
610
|
def read_local_usage(self) -> LocalUsageSnapshot | None:
|
|
567
611
|
root = Path(os.environ.get("GEMINI_HOME", Path.home() / ".gemini"))
|
|
568
612
|
newest: tuple[Path, dict[str, Any], datetime | None] | None = None
|
|
@@ -591,6 +635,19 @@ class GeminiUsageProvider(ModelUsageProvider):
|
|
|
591
635
|
)
|
|
592
636
|
|
|
593
637
|
|
|
638
|
+
def _gemini_model_stats(data: dict[str, Any]) -> tuple[str | None, dict[str, int]]:
|
|
639
|
+
stats = data.get("stats") if isinstance(data.get("stats"), dict) else {}
|
|
640
|
+
models = stats.get("models") if isinstance(stats.get("models"), dict) else {}
|
|
641
|
+
if not models:
|
|
642
|
+
return None, {}
|
|
643
|
+
|
|
644
|
+
model_name = next(iter(models))
|
|
645
|
+
model_stats = models.get(model_name)
|
|
646
|
+
if not isinstance(model_stats, dict):
|
|
647
|
+
return str(model_name), {}
|
|
648
|
+
return str(model_name), _normalize_token_dict(model_stats.get("tokens"))
|
|
649
|
+
|
|
650
|
+
|
|
594
651
|
_USAGE_PROVIDERS: dict[ModelName, ModelUsageProvider] = {
|
|
595
652
|
ModelName.CLAUDE: ClaudeUsageProvider(),
|
|
596
653
|
ModelName.CODEX: CodexUsageProvider(),
|
|
@@ -14,7 +14,6 @@ from app.telegram.commands.base import (
|
|
|
14
14
|
from app.telegram.commands.branch import BranchCommand, PrCommand, PullCommand, RebaseCommand
|
|
15
15
|
from app.telegram.commands.clear_stop import ClearCommand, StopCommand
|
|
16
16
|
from app.telegram.commands.fix import (
|
|
17
|
-
FIX_COMMIT_PENDING_ACTION,
|
|
18
17
|
FIX_SOURCE_AWAIT_ACTION,
|
|
19
18
|
FIX_SOURCE_PENDING_ACTION,
|
|
20
19
|
FixCommand,
|
|
@@ -37,7 +36,6 @@ __all__ = [
|
|
|
37
36
|
"CommandRegistry",
|
|
38
37
|
"CommandResponse",
|
|
39
38
|
"ConfirmableCommand",
|
|
40
|
-
"FIX_COMMIT_PENDING_ACTION",
|
|
41
39
|
"FIX_SOURCE_AWAIT_ACTION",
|
|
42
40
|
"FIX_SOURCE_PENDING_ACTION",
|
|
43
41
|
"FixCommand",
|
|
@@ -80,7 +80,8 @@ HELP_TEXT = "\n".join(
|
|
|
80
80
|
"- no commit",
|
|
81
81
|
"- plan: <natural language> or /plan <natural language> - plan mode (plan only; no code changes)",
|
|
82
82
|
"- ask: <natural language> or /ask <natural language> - ask mode (analysis and answers; no code edits)",
|
|
83
|
-
"-
|
|
83
|
+
"- fix: <natural language> or /fix - fix mode (reply to a job result; amends that commit)",
|
|
84
|
+
"- Korean aliases 계획:, 질문:, and 수정: instead of plan:/ask:/fix: (colons `:` or full-width `:` allowed)",
|
|
84
85
|
"",
|
|
85
86
|
"Commands:",
|
|
86
87
|
"- /model <claude|codex|gemini>: Change the default model",
|
|
@@ -94,7 +95,7 @@ HELP_TEXT = "\n".join(
|
|
|
94
95
|
"- /reports [count]: Conversation memory report",
|
|
95
96
|
"- /init: Reset this chat's settings",
|
|
96
97
|
"- /stop <job_id>: Stop a running job",
|
|
97
|
-
"- /fix
|
|
98
|
+
"- /fix: Fix the linked job commit (reply to a job result first)",
|
|
98
99
|
"- /start: Inline menu",
|
|
99
100
|
]
|
|
100
101
|
)
|
|
@@ -147,6 +148,22 @@ HELP_ASK_TOPIC = "\n".join(
|
|
|
147
148
|
]
|
|
148
149
|
)
|
|
149
150
|
|
|
151
|
+
HELP_FIX_TOPIC = "\n".join(
|
|
152
|
+
[
|
|
153
|
+
"Fix mode (fix)",
|
|
154
|
+
"",
|
|
155
|
+
"Apply follow-up fixes on top of a previous succeeded job. Reply to a job result, then use "
|
|
156
|
+
"fix: or /fix. The agent amends the existing commit and pushes with --force-with-lease.",
|
|
157
|
+
"",
|
|
158
|
+
"Examples",
|
|
159
|
+
"- (reply to job result) fix: add missing tests",
|
|
160
|
+
"- (reply to job result) /fix then send the fix instruction",
|
|
161
|
+
"- (reply to job result) 수정:로그인 검증 버그도 고쳐줘",
|
|
162
|
+
"",
|
|
163
|
+
"See /help for more options.",
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
|
|
150
167
|
|
|
151
168
|
def _button_rows(buttons: list[InlineButton], per_row: int = 2) -> list[list[InlineButton]]:
|
|
152
169
|
return [buttons[i : i + per_row] for i in range(0, len(buttons), per_row)]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from app.telegram.commands.base import TelegramCommand, TelegramMessage, CommandContext
|
|
4
|
+
|
|
5
|
+
FIX_SOURCE_AWAIT_ACTION = "fix_source_await_instruction"
|
|
6
|
+
FIX_SOURCE_PENDING_ACTION = "fix_source"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FixCommand(TelegramCommand):
|
|
10
|
+
name = "/fix"
|
|
11
|
+
menu_text = "Fix mode amends the linked job commit (reply to a job result first)."
|
|
12
|
+
description = "Fix the linked job commit (reply to a job result, then /fix or fix:)"
|
|
13
|
+
|
|
14
|
+
def execute(self, message: TelegramMessage, ctx: CommandContext) -> str:
|
|
15
|
+
_ = (message, ctx)
|
|
16
|
+
return (
|
|
17
|
+
"Fix mode requires replying to a job result message.\n\n"
|
|
18
|
+
"Example: reply to a job result, then send /fix or fix: add missing tests"
|
|
19
|
+
)
|
|
@@ -37,7 +37,7 @@ class CommandRegistry:
|
|
|
37
37
|
ctx.confirmation_store.pop(scope_project, message.chat_id)
|
|
38
38
|
return init_cmd.execute(message, ctx)
|
|
39
39
|
|
|
40
|
-
if head in {"/plan", "/ask"}:
|
|
40
|
+
if head in {"/plan", "/ask", "/fix"}:
|
|
41
41
|
return None
|
|
42
42
|
|
|
43
43
|
pending = ctx.confirmation_store.get(scope_project, message.chat_id)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from app import __version__
|
|
3
4
|
from app.telegram.commands.base import (
|
|
4
5
|
HELP_AGENT_TOPIC,
|
|
5
6
|
HELP_ASK_TOPIC,
|
|
7
|
+
HELP_FIX_TOPIC,
|
|
6
8
|
HELP_PLAN_TOPIC,
|
|
7
9
|
HELP_TEXT,
|
|
8
10
|
CommandContext,
|
|
@@ -26,6 +28,10 @@ class StartCommand(TelegramCommand):
|
|
|
26
28
|
"modes": "Choose a mode guide.",
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _ready_line() -> str:
|
|
33
|
+
return f"✅ Remote AI Coder v{__version__} is ready."
|
|
34
|
+
|
|
29
35
|
def execute(self, message: TelegramMessage, ctx: CommandContext) -> str:
|
|
30
36
|
tokens = message.text.strip().split()
|
|
31
37
|
if len(tokens) == 2:
|
|
@@ -35,11 +41,11 @@ class StartCommand(TelegramCommand):
|
|
|
35
41
|
return topic_text
|
|
36
42
|
project_name = effective_project_name_for_chat(ctx, message.chat_id)
|
|
37
43
|
if not project_name:
|
|
38
|
-
return "
|
|
44
|
+
return f"{self._ready_line()}\n\nWelcome to Remote AI Coder."
|
|
39
45
|
entry = ctx.project_registry.get(project_name)
|
|
40
46
|
if not entry:
|
|
41
47
|
return (
|
|
42
|
-
"
|
|
48
|
+
f"{self._ready_line()}\n\n"
|
|
43
49
|
"Welcome to Remote AI Coder.\n"
|
|
44
50
|
f"- Project: {project_name} (not registered)"
|
|
45
51
|
)
|
|
@@ -50,7 +56,7 @@ class StartCommand(TelegramCommand):
|
|
|
50
56
|
state = "enabled" if entry.enabled else "disabled"
|
|
51
57
|
return "\n".join(
|
|
52
58
|
[
|
|
53
|
-
|
|
59
|
+
self._ready_line(),
|
|
54
60
|
"",
|
|
55
61
|
"Welcome to Remote AI Coder.",
|
|
56
62
|
f"- Project: {entry.name}",
|
|
@@ -98,6 +104,7 @@ class StartCommand(TelegramCommand):
|
|
|
98
104
|
InlineButton("AGENTS mode", "/help agent"),
|
|
99
105
|
InlineButton("PLAN mode", "/help plan"),
|
|
100
106
|
InlineButton("ASK mode", "/help ask"),
|
|
107
|
+
InlineButton("FIX mode", "/help fix"),
|
|
101
108
|
],
|
|
102
109
|
[InlineButton("Back", "/start")],
|
|
103
110
|
]
|
|
@@ -117,7 +124,7 @@ class HelpCommand(TelegramCommand):
|
|
|
117
124
|
tokens = message.text.strip().split()
|
|
118
125
|
if len(tokens) >= 2:
|
|
119
126
|
raw = tokens[1]
|
|
120
|
-
topic_aliases = {"에이전트": "agent", "계획": "plan", "질문": "ask"}
|
|
127
|
+
topic_aliases = {"에이전트": "agent", "계획": "plan", "질문": "ask", "수정": "fix"}
|
|
121
128
|
topic = topic_aliases.get(raw, raw.lower())
|
|
122
129
|
if topic in ("agent", "agents"):
|
|
123
130
|
return HELP_AGENT_TOPIC
|
|
@@ -125,6 +132,8 @@ class HelpCommand(TelegramCommand):
|
|
|
125
132
|
return HELP_PLAN_TOPIC
|
|
126
133
|
if topic == "ask":
|
|
127
134
|
return HELP_ASK_TOPIC
|
|
135
|
+
if topic == "fix":
|
|
136
|
+
return HELP_FIX_TOPIC
|
|
128
137
|
if len(tokens) >= 2 and self._registry is not None:
|
|
129
138
|
subcmd = self._registry.get("/" + tokens[1])
|
|
130
139
|
if subcmd is not None and subcmd.menu_text:
|
|
@@ -141,7 +150,7 @@ class HelpCommand(TelegramCommand):
|
|
|
141
150
|
tokens = message.text.strip().split() if message else []
|
|
142
151
|
if len(tokens) >= 2:
|
|
143
152
|
topic = tokens[1].lower()
|
|
144
|
-
if topic in ("agent", "agents", "plan", "ask"):
|
|
153
|
+
if topic in ("agent", "agents", "plan", "ask", "fix"):
|
|
145
154
|
return [[InlineButton("← Back", "/help")]]
|
|
146
155
|
subcmd = self._registry.get("/" + tokens[1])
|
|
147
156
|
if subcmd is not None:
|