From db56aabe5317daf1c342c7b2cf7a1d9c140a0a15 Mon Sep 17 00:00:00 2001 From: baramgay Date: Thu, 11 Jun 2026 23:45:30 +0900 Subject: [PATCH] feat: add shell completion for update_status.py via argcomplete (closes #2) --- requirements.txt | 3 + scripts/install_completion.py | 144 ++++++++++++++++++++++++++++++++++ scripts/update_status.py | 127 +++++++++++++++++++++--------- 3 files changed, 237 insertions(+), 37 deletions(-) create mode 100644 scripts/install_completion.py diff --git a/requirements.txt b/requirements.txt index 21b1fea..db1df3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ +# Shell tab-completion for update_status.py (optional but recommended) +argcomplete>=3.0.0 + fastapi>=0.110.0 uvicorn[standard]>=0.27.0 pydantic>=2.0.0 diff --git a/scripts/install_completion.py b/scripts/install_completion.py new file mode 100644 index 0000000..aae5395 --- /dev/null +++ b/scripts/install_completion.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Install shell completion for update_status.py. + +Generates tab-completion scripts for bash, zsh, and fish using +register-python-argcomplete (ships with argcomplete). + +Usage: + python scripts/install_completion.py --bash # append snippet to ~/.bashrc + python scripts/install_completion.py --zsh # append snippet to ~/.zshrc + python scripts/install_completion.py --fish # write fish completion file + python scripts/install_completion.py --print # print snippet, no changes + +After running, open a new shell (or source the config file) and tab away: + + python scripts/update_status.py [TAB] + # orchestrator lead-data lead-dev eda-analyst backend ... +""" + +import subprocess +import sys +from pathlib import Path + + +SCRIPT_PATH = Path(__file__).parent / "update_status.py" +COMPLETION_CMD = "python " + str(SCRIPT_PATH) + + +def check_argcomplete(): + """Check argcomplete is installed; offer to install it if missing.""" + try: + import argcomplete # noqa: F401 + return True + except ImportError: + print("argcomplete is not installed.") + answer = input("Install it now with pip? [Y/n]: ").strip().lower() + if answer in ("", "y", "yes"): + result = subprocess.run( + [sys.executable, "-m", "pip", "install", "argcomplete>=3.0.0"], + check=False, + ) + if result.returncode == 0: + print("argcomplete installed successfully.") + return True + else: + print("Installation failed. Please run: pip install argcomplete") + return False + else: + print("Skipped. Run 'pip install argcomplete' when ready.") + return False + + +def generate_bash_snippet(): + """Return the bash/zsh eval snippet for update_status.py completion.""" + return ( + f'\n# agentops shell completion (update_status.py)\n' + f'eval "$(register-python-argcomplete \'{COMPLETION_CMD}\')"\n' + ) + + +def generate_fish_snippet(): + """Return the fish completion script content.""" + return ( + f"# agentops shell completion (update_status.py)\n" + f"complete -c update_status.py -f\n" + f"# Use register-python-argcomplete for full dynamic completion:\n" + f"register-python-argcomplete --shell fish '{COMPLETION_CMD}' | source\n" + ) + + +def append_to_file(path: Path, snippet: str, label: str): + """Append snippet to a shell config file if not already present.""" + path = path.expanduser() + marker = "agentops shell completion" + if path.exists(): + existing = path.read_text(encoding="utf-8") + if marker in existing: + print(f"Completion already registered in {path} -- skipping.") + return + with open(path, "a", encoding="utf-8") as f: + f.write(snippet) + print(f"Completion snippet appended to {path}") + print(f"Run: source {path} (or open a new {label} shell)") + + +def install_fish(): + """Write fish completion file to the standard completions directory.""" + fish_dir = Path("~/.config/fish/completions").expanduser() + fish_dir.mkdir(parents=True, exist_ok=True) + target = fish_dir / "update_status.py.fish" + snippet = generate_fish_snippet() + if target.exists(): + existing = target.read_text(encoding="utf-8") + if "agentops shell completion" in existing: + print(f"Fish completion already installed at {target} -- skipping.") + return + target.write_text(snippet, encoding="utf-8") + print(f"Fish completion installed at {target}") + print("It will be active in new fish shells automatically.") + + +def print_snippet(is_fish: bool): + """Print the completion snippet without making any file changes.""" + if is_fish: + snippet = generate_fish_snippet() + print("\n--- fish completion (save to ~/.config/fish/completions/update_status.py.fish) ---") + else: + snippet = generate_bash_snippet() + print("\n--- bash/zsh completion snippet (append to ~/.bashrc or ~/.zshrc) ---") + print(snippet) + + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description="Install shell completion for update_status.py.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument("--bash", action="store_true", help="Install for bash (~/.bashrc)") + group.add_argument("--zsh", action="store_true", help="Install for zsh (~/.zshrc)") + group.add_argument("--fish", action="store_true", help="Install for fish (~/.config/fish/completions/)") + group.add_argument("--print", dest="print_only", action="store_true", help="Print snippet only -- no files modified") + + args = parser.parse_args() + + if args.print_only: + print_snippet(is_fish=args.fish) + return + + if not check_argcomplete(): + sys.exit(1) + + if args.bash: + append_to_file(Path("~/.bashrc"), generate_bash_snippet(), "bash") + elif args.zsh: + append_to_file(Path("~/.zshrc"), generate_bash_snippet(), "zsh") + elif args.fish: + install_fish() + + +if __name__ == "__main__": + main() diff --git a/scripts/update_status.py b/scripts/update_status.py index c2706ce..7cabadd 100644 --- a/scripts/update_status.py +++ b/scripts/update_status.py @@ -1,29 +1,37 @@ """ -에이전트 상태 업데이트 유틸리티 - -사용법: - 기본: python scripts/update_status.py [agent_id] [status] "[task]" - 학습: python scripts/update_status.py [agent_id] done "[task]" --learn "[학습 메모]" - -status 값: - working - 작업 중 (초록) - review - 검토 중 (노랑) - waiting - 대기 중 (주황) - done - 완료 (다시 idle로) - idle - 유휴 - -예시: - python scripts/update_status.py data-collector working "공공데이터포털 인구 데이터 수집 중" - python scripts/update_status.py data-collector done "인구 데이터 수집 완료 (3,842건)" - python scripts/update_status.py reporter done "청년정착 보고서 완료" --learn "Ⅴ 결론 y불릿은 4개씩 끊는게 한 페이지에 맞음" +Agent status update utility + +Usage: + basic: python scripts/update_status.py [agent_id] [status] "[task]" + learn: python scripts/update_status.py [agent_id] done "[task]" --learn "[memo]" + +status values: + working - in progress (green) + review - under review (yellow) + waiting - waiting (orange) + done - complete (returns to idle) + idle - idle + +examples: + python scripts/update_status.py data-collector working "Collecting population data" + python scripts/update_status.py data-collector done "Collected population data (3,842 records)" + python scripts/update_status.py reporter done "Report complete" --learn "4 bullets per section fits one page" """ import sys import json import os +import argparse from datetime import datetime from pathlib import Path +# Optional argcomplete support for shell tab-completion +try: + import argcomplete + HAS_ARGCOMPLETE = True +except ImportError: + HAS_ARGCOMPLETE = False + # Windows 콘솔(cp949)에서 이모지·한글 출력 시 UnicodeEncodeError 방지 try: sys.stdout.reconfigure(encoding="utf-8") @@ -69,29 +77,80 @@ def append_work_log(agent_id, status, content): } -def main(): - if len(sys.argv) < 3: - print("사용법: python update_status.py [agent_id] [status] [task_description]") - sys.exit(1) +def get_agent_ids(): + """Return list of valid agent IDs from agent_status.json for shell completion.""" + try: + find_status_file() + data = load_status() + return list(data.get("agents", {}).keys()) + except Exception: + return [] + - agent_id = sys.argv[1] - status = sys.argv[2].lower() - # task는 4번째 인자로 받되, --learn 옵션 자체나 그 값은 제외 - task = "" - if len(sys.argv) > 3 and sys.argv[3] != "--learn": - task = sys.argv[3] +def main(): + # Build argparse parser with dynamic agent ID choices for completion + _agent_ids = get_agent_ids() + + parser = argparse.ArgumentParser( + description="Update agent status in the agentops system.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + "status values:\n" + " working in progress\n" + " review under review\n" + " waiting waiting on dependency\n" + " done complete (transitions to idle)\n" + " idle idle / standby\n\n" + "examples:\n" + " python scripts/update_status.py eda-analyst working \"Analyzing Q1 data\"\n" + " python scripts/update_status.py eda-analyst done \"Analysis complete\"\n" + " python scripts/update_status.py reporter done \"Report done\" --learn \"4 bullets fits one page\"" + ), + ) + parser.add_argument( + "agent_id", + choices=_agent_ids if _agent_ids else None, + metavar="agent_id", + help="Agent ID (e.g. orchestrator, backend, eda-analyst). Tab-complete for full list.", + ) + parser.add_argument( + "status", + choices=["working", "review", "waiting", "done", "idle"], + help="New status for the agent.", + ) + parser.add_argument( + "task", + nargs="?", + default="", + help="Task description (optional, quoted string).", + ) + parser.add_argument( + "--learn", + metavar="MEMO", + help="Learning memo to append to agent memory.md (used with status=done).", + ) + + # Register completion handler before parsing — no-op when argcomplete is absent + if HAS_ARGCOMPLETE: + argcomplete.autocomplete(parser) + + args = parser.parse_args() + agent_id = args.agent_id + status = args.status.lower() + task = args.task or "" + learn_text = args.learn if status not in VALID_STATUSES: - print(f"오류: status는 {VALID_STATUSES} 중 하나여야 합니다.") + print(f"Error: status must be one of {VALID_STATUSES}") sys.exit(1) - # 파일 존재 확인 (common_io가 없으면 생성) + # Ensure status file exists find_status_file() data = load_status() if agent_id not in data.get("agents", {}): - print(f"오류: 알 수 없는 에이전트 ID '{agent_id}'") + print(f"Error: unknown agent ID '{agent_id}'") sys.exit(1) now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") @@ -116,13 +175,7 @@ def main(): data.setdefault("log", []).insert(0, log_entry) data["log"] = data["log"][:30] - # --learn 옵션 처리 (status가 done인 경우만) - learn_text = None - if '--learn' in sys.argv: - idx = sys.argv.index('--learn') - if idx + 1 < len(sys.argv): - learn_text = sys.argv[idx + 1] - + # Process --learn option (only when status is done) if learn_text and status == 'done': import re hanja_re = re.compile(r'[㐀-䶿一-鿿]')