remote-coder 0.5.1__tar.gz → 0.5.3__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.5.1 → remote_coder-0.5.3}/PKG-INFO +6 -3
- {remote_coder-0.5.1 → remote_coder-0.5.3}/README.md +5 -2
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/__init__.py +1 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/router.py +2 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/i18n.js +5 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/admin.html +4 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/base.py +100 -16
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/codex.py +3 -3
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/gemini.py +2 -2
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/cli.py +5 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/diagnostics.py +12 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/execution_pipeline.py +6 -5
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/fix_pipeline.py +3 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/manager.py +13 -0
- remote_coder-0.5.3/app/jobs/result_writer.py +223 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/schemas.py +8 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/store.py +43 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/worktree_planner.py +2 -2
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/main.py +40 -1
- remote_coder-0.5.3/app/system_startup.py +94 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/__init__.py +2 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/base.py +22 -4
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/branch.py +24 -4
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/registry.py +10 -2
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/status.py +70 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/system.py +13 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/command_flow.py +1 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/job_submission.py +50 -64
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/natural_flow.py +6 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/presenters.py +9 -6
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/update_handler.py +7 -2
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/i18n.py +25 -10
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/messages.py +12 -5
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/notifier.py +9 -2
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/notifier_protocol.py +8 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/parser.py +14 -12
- {remote_coder-0.5.1 → remote_coder-0.5.3}/pyproject.toml +1 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/PKG-INFO +6 -3
- {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/SOURCES.txt +4 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_admin_router.py +2 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_ai_base.py +8 -0
- remote_coder-0.5.3/tests/test_base_runner.py +43 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_claude_runner.py +24 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_cli.py +2 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_codex_runner.py +23 -0
- remote_coder-0.5.3/tests/test_command_flow.py +83 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_command_parser.py +25 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_commands.py +282 -4
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_diagnostics.py +14 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_gemini_runner.py +26 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_job_manager.py +145 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_job_store.py +108 -0
- remote_coder-0.5.3/tests/test_job_submission.py +44 -0
- remote_coder-0.5.3/tests/test_main_lifespan.py +99 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_notifier.py +40 -5
- remote_coder-0.5.3/tests/test_result_writer.py +68 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_system_startup.py +83 -1
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_webhook.py +109 -2
- remote_coder-0.5.1/app/jobs/result_writer.py +0 -104
- remote_coder-0.5.1/app/system_startup.py +0 -34
- remote_coder-0.5.1/tests/test_main_lifespan.py +0 -67
- {remote_coder-0.5.1 → remote_coder-0.5.3}/LICENSE +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/advanced_settings.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/database_browser.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/admin.css +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/advanced.svg +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/database.svg +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/download.svg +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/home.svg +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/logs.svg +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/icons/projects.svg +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/static/summary.js +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/advanced.html +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/database.html +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/logs.html +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/admin/templates/projects.html +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/claude.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/factory.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/model_catalog.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/ai/usage.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/config.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/ai_commit.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/branch_naming.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/commit_message.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/service.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/git/worktree_service.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/fix_support.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/heartbeat.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/jobs/plan_decisions.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/models.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/code.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/events.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/git.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/log_buffer.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/memory.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/monitoring/model.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/projects/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/projects/registry.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/security/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/security/auth.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/bot_instances.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/clear_stop.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/fix.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/model.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/commands/monitor.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/confirmations.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/collaborators.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/context.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/models.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/protocols.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/sqlite_rows.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/sqlite_store.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/conversation/store.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/formatting.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/__init__.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/callback_dispatcher.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/fix_flow.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/plan_flow.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/recent_updates.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/request.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/handlers/session_binding.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/lists.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/model_preferences.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/plan_decisions_flow.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/webhook.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/telegram/webhook_registration.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/app/tunnel.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/dependency_links.txt +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/entry_points.txt +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/requires.txt +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/remote_coder.egg-info/top_level.txt +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/setup.cfg +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_ai_commit.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_ai_factory.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_auth.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_bot_instance_manager.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_branch_naming.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_commit_message_formatter.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_config.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_conversation_store.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_database_browser.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_event_logger.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_git_service.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_i18n.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_job_status.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_lists.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_log_buffer.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_monitoring.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_plan_decisions.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_project_registry.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_project_scoped_state.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_telegram_formatting.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_tunnel.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_webhook_multibot.py +0 -0
- {remote_coder-0.5.1 → remote_coder-0.5.3}/tests/test_webhook_registration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remote-coder
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Telegram-based remote AI coding automation server
|
|
5
5
|
Author: Remote AI Coder contributors
|
|
6
6
|
License: Apache License
|
|
@@ -236,7 +236,7 @@ Run Claude Code, Codex, or Gemini on your local development machine by sending a
|
|
|
236
236
|
- Claude, Codex, and Gemini runners behind the same job flow.
|
|
237
237
|
- Reply-linked jobs continue the same AI CLI session, so follow-ups build on prior context.
|
|
238
238
|
- Local admin UI for project setup, advanced settings, logs, and conversation memory.
|
|
239
|
-
- Read-only `plan
|
|
239
|
+
- Read-only `plan:`, `ask:`, and `research:` modes when you want analysis without commits. PLAN mode asks open decisions through inline buttons first, then finalizes the plan from your answers; RESEARCH mode asks the selected AI CLI to use internet search when useful.
|
|
240
240
|
|
|
241
241
|
## Quick Start
|
|
242
242
|
|
|
@@ -292,7 +292,8 @@ Each project uses its own bot. The webhook path contains the first 16 hex charac
|
|
|
292
292
|
|---|---|
|
|
293
293
|
| `/start`, `/help` | Open the menu or command help |
|
|
294
294
|
| `/model` | View or change the chat's default model |
|
|
295
|
-
| `/status [job_id]` | Inspect recent or specific jobs |
|
|
295
|
+
| `/status [job_id]` | Inspect recent or specific jobs; running jobs show the latest captured AI output when available |
|
|
296
|
+
| `/log [job_id]` | Pick a recent job or show captured AI stdout for a specific job |
|
|
296
297
|
| `/branch [name]` | Show or switch the bound project's local branch |
|
|
297
298
|
| `/pull` | Fetch remotes and pull the current branch |
|
|
298
299
|
| `/rebase [branch]` | Rebase and fast-forward a completed branch into `main` or `master` |
|
|
@@ -311,6 +312,7 @@ Natural-language examples:
|
|
|
311
312
|
Fix the login validation bug with model: codex
|
|
312
313
|
plan: outline the migration before changing code
|
|
313
314
|
/ask what test command does this repo use?
|
|
315
|
+
/research compare current Telegram webhook security guidance
|
|
314
316
|
수정: 방금 작업에서 README 문구만 더 간결하게 바꿔줘
|
|
315
317
|
```
|
|
316
318
|
|
|
@@ -321,6 +323,7 @@ Day-to-day setup happens in the local admin UI. Files live under `REMOTE_CODER_H
|
|
|
321
323
|
- `projects.json` stores project records, bot tokens, allowlists, root paths, and default models.
|
|
322
324
|
- `advanced_settings.json` stores global behavior such as timeouts, sandbox mode, language, worktree retention, and memory limits.
|
|
323
325
|
- `worktrees/<project>/` contains managed job worktrees and logs.
|
|
326
|
+
- On server startup, queued Jobs stored in SQLite are rerun; Jobs that were running when the server stopped are marked failed with `server_restart` for `/status` review.
|
|
324
327
|
|
|
325
328
|
Useful overrides: `REMOTE_CODER_HOME`, `PROJECTS_CONFIG_PATH`, `CONVERSATION_DB_PATH`, and `JOB_DB_PATH`.
|
|
326
329
|
|
|
@@ -15,7 +15,7 @@ Run Claude Code, Codex, or Gemini on your local development machine by sending a
|
|
|
15
15
|
- Claude, Codex, and Gemini runners behind the same job flow.
|
|
16
16
|
- Reply-linked jobs continue the same AI CLI session, so follow-ups build on prior context.
|
|
17
17
|
- Local admin UI for project setup, advanced settings, logs, and conversation memory.
|
|
18
|
-
- Read-only `plan
|
|
18
|
+
- Read-only `plan:`, `ask:`, and `research:` modes when you want analysis without commits. PLAN mode asks open decisions through inline buttons first, then finalizes the plan from your answers; RESEARCH mode asks the selected AI CLI to use internet search when useful.
|
|
19
19
|
|
|
20
20
|
## Quick Start
|
|
21
21
|
|
|
@@ -71,7 +71,8 @@ Each project uses its own bot. The webhook path contains the first 16 hex charac
|
|
|
71
71
|
|---|---|
|
|
72
72
|
| `/start`, `/help` | Open the menu or command help |
|
|
73
73
|
| `/model` | View or change the chat's default model |
|
|
74
|
-
| `/status [job_id]` | Inspect recent or specific jobs |
|
|
74
|
+
| `/status [job_id]` | Inspect recent or specific jobs; running jobs show the latest captured AI output when available |
|
|
75
|
+
| `/log [job_id]` | Pick a recent job or show captured AI stdout for a specific job |
|
|
75
76
|
| `/branch [name]` | Show or switch the bound project's local branch |
|
|
76
77
|
| `/pull` | Fetch remotes and pull the current branch |
|
|
77
78
|
| `/rebase [branch]` | Rebase and fast-forward a completed branch into `main` or `master` |
|
|
@@ -90,6 +91,7 @@ Natural-language examples:
|
|
|
90
91
|
Fix the login validation bug with model: codex
|
|
91
92
|
plan: outline the migration before changing code
|
|
92
93
|
/ask what test command does this repo use?
|
|
94
|
+
/research compare current Telegram webhook security guidance
|
|
93
95
|
수정: 방금 작업에서 README 문구만 더 간결하게 바꿔줘
|
|
94
96
|
```
|
|
95
97
|
|
|
@@ -100,6 +102,7 @@ Day-to-day setup happens in the local admin UI. Files live under `REMOTE_CODER_H
|
|
|
100
102
|
- `projects.json` stores project records, bot tokens, allowlists, root paths, and default models.
|
|
101
103
|
- `advanced_settings.json` stores global behavior such as timeouts, sandbox mode, language, worktree retention, and memory limits.
|
|
102
104
|
- `worktrees/<project>/` contains managed job worktrees and logs.
|
|
105
|
+
- On server startup, queued Jobs stored in SQLite are rerun; Jobs that were running when the server stopped are marked failed with `server_restart` for `/status` review.
|
|
103
106
|
|
|
104
107
|
Useful overrides: `REMOTE_CODER_HOME`, `PROJECTS_CONFIG_PATH`, `CONVERSATION_DB_PATH`, and `JOB_DB_PATH`.
|
|
105
108
|
|
|
@@ -378,9 +378,10 @@ def create_admin_router(
|
|
|
378
378
|
report = check_prerequisites()
|
|
379
379
|
installed = [cli.name for cli in report.ai_clis if cli.installed]
|
|
380
380
|
_adminlog.info(
|
|
381
|
-
"prerequisites queried ngrok_ok=%s ai_clis=%s",
|
|
381
|
+
"prerequisites queried ngrok_ok=%s ai_clis=%s github_cli=%s",
|
|
382
382
|
report.ngrok_ok,
|
|
383
383
|
",".join(installed) or "-",
|
|
384
|
+
report.github_cli.installed,
|
|
384
385
|
)
|
|
385
386
|
return report.model_dump()
|
|
386
387
|
|
|
@@ -146,6 +146,11 @@
|
|
|
146
146
|
en: "None found (install at least one: claude / codex / gemini)",
|
|
147
147
|
ko: "설치된 것 없음 (claude / codex / gemini 중 최소 1개 설치)",
|
|
148
148
|
},
|
|
149
|
+
"setup.githubCli": { en: "GitHub CLI", ko: "GitHub CLI" },
|
|
150
|
+
"setup.githubCliNone": {
|
|
151
|
+
en: "Not found (install gh before using /pr)",
|
|
152
|
+
ko: "설치되지 않음 (/pr 사용 전 gh 설치 필요)",
|
|
153
|
+
},
|
|
149
154
|
"setup.checking": { en: "Checking…", ko: "확인 중…" },
|
|
150
155
|
"setup.recheck": { en: "Re-check", ko: "다시 확인" },
|
|
151
156
|
"setup.cta": { en: "Add your first project", ko: "첫 프로젝트 추가" },
|
|
@@ -248,9 +248,12 @@
|
|
|
248
248
|
const installed = (report.ai_clis || []).filter((c) => c.installed).map((c) => c.name);
|
|
249
249
|
const aiOk = installed.length > 0;
|
|
250
250
|
const aiDetail = aiOk ? installed.join(", ") : i18n.t("setup.aiCliNone");
|
|
251
|
+
const ghOk = !!(report.github_cli && report.github_cli.installed);
|
|
252
|
+
const ghDetail = ghOk ? "gh" : i18n.t("setup.githubCliNone");
|
|
251
253
|
$("#setup-prereq").innerHTML =
|
|
252
254
|
renderPrereqRow(!!report.ngrok_ok, i18n.t("setup.ngrok"), ngrokDetail) +
|
|
253
|
-
renderPrereqRow(aiOk, i18n.t("setup.aiCli"), aiDetail)
|
|
255
|
+
renderPrereqRow(aiOk, i18n.t("setup.aiCli"), aiDetail) +
|
|
256
|
+
renderPrereqRow(ghOk, i18n.t("setup.githubCli"), ghDetail);
|
|
254
257
|
}
|
|
255
258
|
|
|
256
259
|
async function loadPrerequisites() {
|
|
@@ -4,9 +4,11 @@ import re
|
|
|
4
4
|
import subprocess
|
|
5
5
|
import threading
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from dataclasses import dataclass, field
|
|
8
9
|
from datetime import UTC, datetime
|
|
9
10
|
from pathlib import Path
|
|
11
|
+
from typing import Literal
|
|
10
12
|
|
|
11
13
|
from app.jobs.schemas import JobMode
|
|
12
14
|
from app.monitoring.events import EventLogger
|
|
@@ -57,6 +59,18 @@ _PLAN_DECISIONS_INSTRUCTION = (
|
|
|
57
59
|
)
|
|
58
60
|
|
|
59
61
|
|
|
62
|
+
_RESEARCH_INSTRUCTION = (
|
|
63
|
+
"You are in RESEARCH mode. Read the repository context and answer the user's research "
|
|
64
|
+
"question. Do not modify files.\n\n"
|
|
65
|
+
"Use internet search when it is useful or necessary for the question, similar to a deep "
|
|
66
|
+
"research workflow. Compare multiple perspectives or sources when possible, and clearly "
|
|
67
|
+
"separate repository-derived facts from external findings. Include citations or source "
|
|
68
|
+
"links for external claims, call out uncertainty or limitations, and finish with a direct "
|
|
69
|
+
"answer to the user's problem.\n\n"
|
|
70
|
+
"User research request:\n"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
60
74
|
def instruction_for_runner_mode(instruction: str, mode: JobMode) -> str:
|
|
61
75
|
if mode == JobMode.PLAN:
|
|
62
76
|
return f"{_PLAN_DECISIONS_INSTRUCTION}{instruction}"
|
|
@@ -66,6 +80,8 @@ def instruction_for_runner_mode(instruction: str, mode: JobMode) -> str:
|
|
|
66
80
|
"Do not modify files.\n\n"
|
|
67
81
|
f"User question:\n{instruction}"
|
|
68
82
|
)
|
|
83
|
+
if mode == JobMode.RESEARCH:
|
|
84
|
+
return f"{_RESEARCH_INSTRUCTION}{instruction}"
|
|
69
85
|
return instruction
|
|
70
86
|
|
|
71
87
|
|
|
@@ -81,6 +97,9 @@ class RunnerInput:
|
|
|
81
97
|
session_id: str | None = None
|
|
82
98
|
resume_token: str | None = None
|
|
83
99
|
native_resume_cwd_stable: bool = True
|
|
100
|
+
output_callback: Callable[[Literal["stdout", "stderr"], str], None] | None = field(
|
|
101
|
+
default=None, compare=False
|
|
102
|
+
)
|
|
84
103
|
|
|
85
104
|
|
|
86
105
|
@dataclass
|
|
@@ -199,23 +218,30 @@ class BaseCliRunner(AiRunner):
|
|
|
199
218
|
cancelled.set()
|
|
200
219
|
|
|
201
220
|
threading.Thread(target=_watch, daemon=True).start()
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
221
|
+
if runner_input.output_callback is None:
|
|
222
|
+
try:
|
|
223
|
+
stdout_data, stderr_data = proc.communicate(timeout=runner_input.timeout_seconds)
|
|
224
|
+
except subprocess.TimeoutExpired as exc:
|
|
225
|
+
proc.kill()
|
|
226
|
+
stdout_data, stderr_data = proc.communicate()
|
|
227
|
+
self._log.warning(
|
|
228
|
+
"timeout after %ds stdout_len=%d stderr_len=%d",
|
|
229
|
+
runner_input.timeout_seconds,
|
|
230
|
+
len(stdout_data),
|
|
231
|
+
len(stderr_data),
|
|
232
|
+
)
|
|
233
|
+
raise RunnerExecutionError(
|
|
234
|
+
f"runner timed out after {runner_input.timeout_seconds}s",
|
|
235
|
+
stdout=stdout_data,
|
|
236
|
+
stderr=stderr_data,
|
|
237
|
+
started_at=started_at,
|
|
238
|
+
) from exc
|
|
239
|
+
else:
|
|
240
|
+
stdout_data, stderr_data = self._communicate_with_output_callback(
|
|
241
|
+
proc,
|
|
242
|
+
runner_input,
|
|
243
|
+
started_at,
|
|
212
244
|
)
|
|
213
|
-
raise RunnerExecutionError(
|
|
214
|
-
f"runner timed out after {runner_input.timeout_seconds}s",
|
|
215
|
-
stdout=stdout_data,
|
|
216
|
-
stderr=stderr_data,
|
|
217
|
-
started_at=started_at,
|
|
218
|
-
) from exc
|
|
219
245
|
finished_at = datetime.now(UTC)
|
|
220
246
|
if cancelled.is_set():
|
|
221
247
|
raise RunnerExecutionError(
|
|
@@ -241,3 +267,61 @@ class BaseCliRunner(AiRunner):
|
|
|
241
267
|
finished_at=finished_at,
|
|
242
268
|
session_id=self._resolve_result_session_id(runner_input, session_files_before),
|
|
243
269
|
)
|
|
270
|
+
|
|
271
|
+
def _communicate_with_output_callback(
|
|
272
|
+
self,
|
|
273
|
+
proc: subprocess.Popen[str],
|
|
274
|
+
runner_input: RunnerInput,
|
|
275
|
+
started_at: datetime,
|
|
276
|
+
) -> tuple[str, str]:
|
|
277
|
+
output_callback = runner_input.output_callback
|
|
278
|
+
if output_callback is None:
|
|
279
|
+
raise ValueError("output_callback is required")
|
|
280
|
+
stdout_parts: list[str] = []
|
|
281
|
+
stderr_parts: list[str] = []
|
|
282
|
+
|
|
283
|
+
def _read_stream(
|
|
284
|
+
stream_name: Literal["stdout", "stderr"],
|
|
285
|
+
parts: list[str],
|
|
286
|
+
) -> None:
|
|
287
|
+
stream = proc.stdout if stream_name == "stdout" else proc.stderr
|
|
288
|
+
if stream is None:
|
|
289
|
+
return
|
|
290
|
+
while True:
|
|
291
|
+
chunk = stream.readline()
|
|
292
|
+
if chunk == "":
|
|
293
|
+
break
|
|
294
|
+
parts.append(chunk)
|
|
295
|
+
try:
|
|
296
|
+
output_callback(stream_name, chunk)
|
|
297
|
+
except Exception: # pylint: disable=broad-except
|
|
298
|
+
self._log.warning("output callback failed stream=%s", stream_name)
|
|
299
|
+
|
|
300
|
+
stdout_thread = threading.Thread(target=_read_stream, args=("stdout", stdout_parts))
|
|
301
|
+
stderr_thread = threading.Thread(target=_read_stream, args=("stderr", stderr_parts))
|
|
302
|
+
stdout_thread.start()
|
|
303
|
+
stderr_thread.start()
|
|
304
|
+
try:
|
|
305
|
+
proc.wait(timeout=runner_input.timeout_seconds)
|
|
306
|
+
except subprocess.TimeoutExpired as exc:
|
|
307
|
+
proc.kill()
|
|
308
|
+
proc.wait()
|
|
309
|
+
stdout_thread.join()
|
|
310
|
+
stderr_thread.join()
|
|
311
|
+
stdout_data = "".join(stdout_parts)
|
|
312
|
+
stderr_data = "".join(stderr_parts)
|
|
313
|
+
self._log.warning(
|
|
314
|
+
"timeout after %ds stdout_len=%d stderr_len=%d",
|
|
315
|
+
runner_input.timeout_seconds,
|
|
316
|
+
len(stdout_data),
|
|
317
|
+
len(stderr_data),
|
|
318
|
+
)
|
|
319
|
+
raise RunnerExecutionError(
|
|
320
|
+
f"runner timed out after {runner_input.timeout_seconds}s",
|
|
321
|
+
stdout=stdout_data,
|
|
322
|
+
stderr=stderr_data,
|
|
323
|
+
started_at=started_at,
|
|
324
|
+
) from exc
|
|
325
|
+
stdout_thread.join()
|
|
326
|
+
stderr_thread.join()
|
|
327
|
+
return "".join(stdout_parts), "".join(stderr_parts)
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from app.ai.base import BaseCliRunner, RunnerInput, instruction_for_runner_mode
|
|
6
|
-
from app.jobs.schemas import
|
|
6
|
+
from app.jobs.schemas import is_read_only_job_mode
|
|
7
7
|
from app.models import CodexSandboxMode
|
|
8
8
|
from app.monitoring.events import EventLogger
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ class CodexRunner(BaseCliRunner):
|
|
|
16
16
|
self._sandbox = sandbox
|
|
17
17
|
|
|
18
18
|
def _resolve_sandbox(self, runner_input: RunnerInput) -> CodexSandboxMode:
|
|
19
|
-
if runner_input.mode
|
|
19
|
+
if is_read_only_job_mode(runner_input.mode):
|
|
20
20
|
return CodexSandboxMode.READ_ONLY
|
|
21
21
|
return self._sandbox
|
|
22
22
|
|
|
@@ -29,7 +29,7 @@ class CodexRunner(BaseCliRunner):
|
|
|
29
29
|
|
|
30
30
|
def build_argv(self, runner_input: RunnerInput) -> list[str]:
|
|
31
31
|
sandbox = self._resolve_sandbox(runner_input)
|
|
32
|
-
if runner_input.mode
|
|
32
|
+
if is_read_only_job_mode(runner_input.mode):
|
|
33
33
|
instruction = instruction_for_runner_mode(runner_input.instruction, runner_input.mode)
|
|
34
34
|
else:
|
|
35
35
|
instruction = runner_input.instruction
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from app.ai.base import BaseCliRunner, RunnerInput, instruction_for_runner_mode
|
|
6
|
-
from app.jobs.schemas import
|
|
6
|
+
from app.jobs.schemas import is_read_only_job_mode
|
|
7
7
|
from app.monitoring.events import EventLogger
|
|
8
8
|
|
|
9
9
|
|
|
@@ -17,7 +17,7 @@ class GeminiRunner(BaseCliRunner):
|
|
|
17
17
|
return Path.home() / ".gemini" / "sessions"
|
|
18
18
|
|
|
19
19
|
def build_argv(self, runner_input: RunnerInput) -> list[str]:
|
|
20
|
-
if runner_input.mode
|
|
20
|
+
if is_read_only_job_mode(runner_input.mode):
|
|
21
21
|
prompt = instruction_for_runner_mode(runner_input.instruction, runner_input.mode)
|
|
22
22
|
argv = ["gemini", "--skip-trust", "-p", prompt]
|
|
23
23
|
else:
|
|
@@ -32,7 +32,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
32
32
|
help="Run the server only (skip ngrok and webhook registration)",
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
subparsers.add_parser("doctor", help="Check prerequisites (ngrok, AI CLIs)")
|
|
35
|
+
subparsers.add_parser("doctor", help="Check prerequisites (ngrok, AI CLIs, GitHub CLI)")
|
|
36
36
|
return parser
|
|
37
37
|
|
|
38
38
|
|
|
@@ -128,6 +128,10 @@ def run_doctor() -> None:
|
|
|
128
128
|
" ⚠️ AI CLI (claude/codex/gemini) not found. Install at least one. "
|
|
129
129
|
"(e.g. npm install -g @anthropic-ai/claude-code)"
|
|
130
130
|
)
|
|
131
|
+
if report.github_cli.installed:
|
|
132
|
+
print(" ✅ GitHub CLI: gh")
|
|
133
|
+
else:
|
|
134
|
+
print(" ⚠️ GitHub CLI (gh) not found. Install it before using /pr.")
|
|
131
135
|
|
|
132
136
|
|
|
133
137
|
if __name__ == "__main__":
|
|
@@ -7,6 +7,7 @@ from pydantic import BaseModel
|
|
|
7
7
|
from app import tunnel
|
|
8
8
|
|
|
9
9
|
AI_CLI_TOOLS = ("claude", "codex", "gemini")
|
|
10
|
+
GITHUB_CLI_TOOL = "gh"
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class AiCliStatus(BaseModel):
|
|
@@ -18,6 +19,7 @@ class PrerequisitesReport(BaseModel):
|
|
|
18
19
|
ngrok_ok: bool
|
|
19
20
|
ngrok_detail: str
|
|
20
21
|
ai_clis: list[AiCliStatus]
|
|
22
|
+
github_cli: AiCliStatus
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
def check_prerequisites() -> PrerequisitesReport:
|
|
@@ -34,4 +36,13 @@ def check_prerequisites() -> PrerequisitesReport:
|
|
|
34
36
|
AiCliStatus(name=tool, installed=shutil.which(tool) is not None)
|
|
35
37
|
for tool in AI_CLI_TOOLS
|
|
36
38
|
]
|
|
37
|
-
|
|
39
|
+
github_cli = AiCliStatus(
|
|
40
|
+
name=GITHUB_CLI_TOOL,
|
|
41
|
+
installed=shutil.which(GITHUB_CLI_TOOL) is not None,
|
|
42
|
+
)
|
|
43
|
+
return PrerequisitesReport(
|
|
44
|
+
ngrok_ok=ngrok_ok,
|
|
45
|
+
ngrok_detail=ngrok_detail,
|
|
46
|
+
ai_clis=ai_clis,
|
|
47
|
+
github_cli=github_cli,
|
|
48
|
+
)
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from app.ai.base import RunnerInput
|
|
7
7
|
from app.git.commit_message import CommitMessageFormatter
|
|
8
8
|
from app.jobs.plan_decisions import PlanDecisionQuestion, parse_plan_decisions
|
|
9
|
-
from app.jobs.schemas import Job, JobMode
|
|
9
|
+
from app.jobs.schemas import Job, JobMode, is_read_only_job_mode
|
|
10
10
|
from app.monitoring.events import EventLogger
|
|
11
11
|
|
|
12
12
|
_joblog = EventLogger("app.jobs.lifecycle", "job.lifecycle")
|
|
@@ -50,7 +50,7 @@ def run_job(manager, job_id: str) -> Job:
|
|
|
50
50
|
created_worktree_for_job = False
|
|
51
51
|
failed_stage: str | None = None
|
|
52
52
|
remote = manager._effective_git_remote_name()
|
|
53
|
-
read_only_job = job.request.mode
|
|
53
|
+
read_only_job = is_read_only_job_mode(job.request.mode)
|
|
54
54
|
plan_decision_questions: list[PlanDecisionQuestion] | None = None
|
|
55
55
|
try:
|
|
56
56
|
job.mark_running()
|
|
@@ -83,6 +83,7 @@ def run_job(manager, job_id: str) -> Job:
|
|
|
83
83
|
len(job.request.instruction),
|
|
84
84
|
**manager._job_ctx(job),
|
|
85
85
|
)
|
|
86
|
+
runner_log = manager._start_incremental_runner_log(job, worktree_base)
|
|
86
87
|
heartbeat = manager._start_heartbeat(job)
|
|
87
88
|
try:
|
|
88
89
|
runner_result = runner.run(
|
|
@@ -97,10 +98,12 @@ def run_job(manager, job_id: str) -> Job:
|
|
|
97
98
|
session_id=job.request.session_id,
|
|
98
99
|
resume_token=job.request.resume_session_token,
|
|
99
100
|
native_resume_cwd_stable=not created_worktree_for_job,
|
|
101
|
+
output_callback=runner_log.output_callback,
|
|
100
102
|
)
|
|
101
103
|
)
|
|
102
104
|
finally:
|
|
103
105
|
heartbeat.set()
|
|
106
|
+
runner_log.flush()
|
|
104
107
|
manager._save_runner_log(job, runner_result, worktree_base)
|
|
105
108
|
_joblog.info(
|
|
106
109
|
"runner exit=%d stdout_len=%d stderr_len=%d",
|
|
@@ -238,9 +241,7 @@ def run_job(manager, job_id: str) -> Job:
|
|
|
238
241
|
finally:
|
|
239
242
|
manager._cancel_events.pop(job_id, None)
|
|
240
243
|
manager._cancelled_job_ids.discard(job_id)
|
|
241
|
-
read_only_succeeded = (
|
|
242
|
-
job.request.mode in (JobMode.PLAN, JobMode.ASK) and job.status.value == "succeeded"
|
|
243
|
-
)
|
|
244
|
+
read_only_succeeded = is_read_only_job_mode(job.request.mode) and job.status.value == "succeeded"
|
|
244
245
|
cleanup_on_success = read_only_succeeded or not manager._effective_keep_worktree_on_success()
|
|
245
246
|
_joblog.info(
|
|
246
247
|
"job finalizing status=%s created_worktree=%s cleanup_on_success=%s",
|
|
@@ -79,6 +79,7 @@ def run_fix_job(manager, job_id: str) -> Job:
|
|
|
79
79
|
runner = manager._runner_factory.create(job.request.model)
|
|
80
80
|
timeout_seconds = manager._effective_job_timeout_seconds()
|
|
81
81
|
fix_prompt = manager.compose_fix_source_prompt(parent_job, job.request.instruction)
|
|
82
|
+
runner_log = manager._start_incremental_runner_log(job, worktree_base)
|
|
82
83
|
heartbeat = manager._start_heartbeat(job)
|
|
83
84
|
try:
|
|
84
85
|
runner_result = runner.run(
|
|
@@ -93,10 +94,12 @@ def run_fix_job(manager, job_id: str) -> Job:
|
|
|
93
94
|
session_id=job.request.session_id,
|
|
94
95
|
resume_token=job.request.resume_session_token,
|
|
95
96
|
native_resume_cwd_stable=not created_worktree_for_job,
|
|
97
|
+
output_callback=runner_log.output_callback,
|
|
96
98
|
)
|
|
97
99
|
)
|
|
98
100
|
finally:
|
|
99
101
|
heartbeat.set()
|
|
102
|
+
runner_log.flush()
|
|
100
103
|
manager._save_runner_log(job, runner_result, worktree_base)
|
|
101
104
|
if runner_result.exit_code != 0:
|
|
102
105
|
raise RuntimeError(runner_result.stderr.strip() or "runner failed")
|
|
@@ -26,6 +26,7 @@ from app.jobs.result_writer import (
|
|
|
26
26
|
make_output_summary,
|
|
27
27
|
preserve_partial_output,
|
|
28
28
|
save_runner_log,
|
|
29
|
+
start_incremental_runner_log,
|
|
29
30
|
strip_links_for_stdout_summary,
|
|
30
31
|
)
|
|
31
32
|
from app.jobs.schemas import FixKind, Job, JobMode, JobRequest
|
|
@@ -186,6 +187,15 @@ class JobManager:
|
|
|
186
187
|
with self._project_lock(job.request.project):
|
|
187
188
|
return run_job(self, job_id)
|
|
188
189
|
|
|
190
|
+
def recover(self, job_id: str) -> Job:
|
|
191
|
+
job = self._job_store.get(job_id)
|
|
192
|
+
if job is None:
|
|
193
|
+
return run_job(self, job_id)
|
|
194
|
+
with self._project_lock(job.request.project):
|
|
195
|
+
if job.request.mode is JobMode.AGENT_FIX:
|
|
196
|
+
return self._run_fix(job_id)
|
|
197
|
+
return run_job(self, job_id)
|
|
198
|
+
|
|
189
199
|
def _project_lock(self, project: str) -> threading.Lock:
|
|
190
200
|
with self._project_locks_guard:
|
|
191
201
|
lock = self._project_locks.get(project)
|
|
@@ -282,6 +292,9 @@ class JobManager:
|
|
|
282
292
|
def _save_runner_log(self, job: Job, runner_result, worktree_base: Path) -> None:
|
|
283
293
|
save_runner_log(job, runner_result, worktree_base)
|
|
284
294
|
|
|
295
|
+
def _start_incremental_runner_log(self, job: Job, worktree_base: Path):
|
|
296
|
+
return start_incremental_runner_log(job, worktree_base, self._job_store.update)
|
|
297
|
+
|
|
285
298
|
@classmethod
|
|
286
299
|
def _strip_links_for_stdout_summary(cls, text: str) -> str:
|
|
287
300
|
return strip_links_for_stdout_summary(text)
|