diff --git a/README.md b/README.md index 51584e5..fc2a7f6 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,11 @@ On Synology/NAS Docker deployments, keep `CODEX_WRITE_SANDBOX=danger-full-access for `/work`. The stricter `workspace-write` sandbox may fail with bubblewrap namespace errors inside the container. +If no `/repo` is set, the bridge tries to auto-select a workspace folder when a +Telegram prompt mentions an exact folder name such as `card_game` or +`nas_connection`. Use `/repo ` when you want to pin the workspace +explicitly. + `/settings`, `/models`, `/schedules`, `/records`, and `/sessions` return inline buttons, so you can change speed/model/reasoning settings, delete schedules, manage personal records, and switch or archive sessions without copying ids. diff --git a/app/bot.py b/app/bot.py index a76604e..8368f30 100644 --- a/app/bot.py +++ b/app/bot.py @@ -945,6 +945,9 @@ class TelegramCodexBot: return selected_sandbox = sandbox or self.config.readonly_sandbox + auto_repo_path = self._auto_select_repo_for_prompt(chat_id, prompt) + if auto_repo_path is not None: + await self._close_client_for_chat(chat_id) preview = _preview_prompt(prompt) task = asyncio.create_task( self._run_codex_job( @@ -961,11 +964,13 @@ class TelegramCodexBot: sandbox=selected_sandbox, ) mode = "쓰기 작업" if selected_sandbox != self.config.readonly_sandbox else "조회 작업" + repo_note = f"\n자동 작업폴더: {auto_repo_path}" if auto_repo_path else "" await self._reply( update, f"{mode}을 시작했습니다.\n" f"요청: {preview}\n" - "완료되면 결과를 보내드릴게요. 진행 중에는 /status로 확인할 수 있습니다.", + "완료되면 결과를 보내드릴게요. 진행 중에는 /status로 확인할 수 있습니다." + f"{repo_note}", ) async def _run_codex_job( @@ -1348,6 +1353,40 @@ class TelegramCodexBot: return False return sandbox == self.config.readonly_sandbox + def _auto_select_repo_for_prompt(self, chat_id: int, prompt: str) -> Path | None: + chat_state = self.state.get_chat(chat_id) + if chat_state.repo_path: + return None + repo_path = self._detect_workspace_repo(prompt) + if repo_path is None: + return None + self.state.set_repo(chat_id, repo_path) + return repo_path + + def _detect_workspace_repo(self, prompt: str) -> Path | None: + root = self.config.workspace_root.resolve() + if not root.exists(): + return None + tokens = _repo_name_tokens(prompt) + if not tokens: + return None + children: dict[str, Path] = {} + try: + for child in root.iterdir(): + if child.is_dir(): + children[child.name.casefold()] = child.resolve() + except OSError: + return None + for token in tokens: + candidate = children.get(token.casefold()) + if candidate and (candidate / ".git").exists(): + return candidate + for token in tokens: + candidate = children.get(token.casefold()) + if candidate: + return candidate + return None + def _resolve_workspace_path(self, requested: str) -> Path: root = self.config.workspace_root.resolve() path = Path(requested) @@ -1388,6 +1427,11 @@ def _preview_prompt(prompt: str, limit: int = 120) -> str: return preview[: limit - 3] + "..." +def _repo_name_tokens(prompt: str) -> list[str]: + tokens = re.findall(r"[A-Za-z0-9][A-Za-z0-9_.-]{1,79}", prompt) + return _dedupe(tokens) + + def _checked_label(label: str, checked: bool) -> str: return f"✓ {label}" if checked else label