fix(bot): 기동 버그 3건 수정 + feat(vikunja): 프로젝트 관리 기능 추가
- fix: apscheduler 누락 의존성 설치 - fix(main): StreamHandler cp949 UnicodeEncodeError 수정 (UTF-8 강제) - fix: workspaces.json 경로 Certes→Variet-Worker 수정 - fix(gemini): MCP issues detected 노이즈 필터 추가 - fix(bot): on_command_error 핸들러 추가 (CommandNotFound 로그 오염 방지) - feat(vikunja): projects 커맨드 (전체 프로젝트 목록+태스크 통계) - feat(vikunja): report 커맨드 (태스크+git log+devlog 종합 현황) - docs(agent): Vikunja 도구 섹션 확장 (12개 커맨드+라벨 가이드) - docs: known-issues 2건 추가, devlog 세션 1 기록
This commit is contained in:
@@ -12,6 +12,9 @@ Usage:
|
||||
python vikunja_helper.py list # List all tasks
|
||||
python vikunja_helper.py list todo # List TODO only
|
||||
python vikunja_helper.py list done # List DONE only
|
||||
python vikunja_helper.py projects # List all Vikunja projects
|
||||
python vikunja_helper.py report # Project status report (current)
|
||||
python vikunja_helper.py report <project_id> # Project status report (specific)
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -170,6 +173,109 @@ def create_task(title: str, description: str = "", done: bool = False, labels: l
|
||||
return result
|
||||
|
||||
|
||||
def list_projects():
|
||||
"""Vikunja 전체 프로젝트 목록 + 태스크 통계."""
|
||||
projects = api_get("/projects")
|
||||
print("📂 프로젝트 목록:")
|
||||
for p in projects:
|
||||
pid = p["id"]
|
||||
title = p["title"]
|
||||
# 각 프로젝트의 태스크 수 조회
|
||||
try:
|
||||
tasks = api_get(f"/projects/{pid}/tasks?per_page=200")
|
||||
todo = sum(1 for t in tasks if not t["done"])
|
||||
done = sum(1 for t in tasks if t["done"])
|
||||
print(f" #{pid:<3d} {title:<30s} TODO: {todo} DONE: {done}")
|
||||
except Exception:
|
||||
print(f" #{pid:<3d} {title:<30s} (조회 실패)")
|
||||
print(f"\n Total: {len(projects)} projects")
|
||||
|
||||
|
||||
def report(project_id: int = None):
|
||||
"""프로젝트 종합 현황 보고 (태스크 + git log + devlog)."""
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
pid = project_id or PROJECT_ID
|
||||
|
||||
# 1) 프로젝트 이름 조회
|
||||
try:
|
||||
projects = api_get("/projects")
|
||||
proj_name = next((p["title"] for p in projects if p["id"] == pid), f"Project #{pid}")
|
||||
except Exception:
|
||||
proj_name = f"Project #{pid}"
|
||||
|
||||
print(f"=== 프로젝트 현황: {proj_name} (#{pid}) ===")
|
||||
|
||||
# 2) 태스크 현황
|
||||
try:
|
||||
tasks = api_get(f"/projects/{pid}/tasks?per_page=200")
|
||||
todo_tasks = [t for t in tasks if not t["done"]]
|
||||
done_tasks = [t for t in tasks if t["done"]]
|
||||
total = len(tasks)
|
||||
rate = f"{len(done_tasks)/total*100:.0f}%" if total else "N/A"
|
||||
|
||||
print(f"\n[태스크]")
|
||||
print(f" TODO: {len(todo_tasks)}건 | DONE: {len(done_tasks)}건 | 완료율: {rate}")
|
||||
|
||||
if todo_tasks:
|
||||
print(f" 미완료:")
|
||||
for t in todo_tasks:
|
||||
labels = ", ".join(l["title"] for l in (t.get("labels") or []))
|
||||
label_str = f" [{labels}]" if labels else ""
|
||||
desc = (t.get("description") or "")[:40].replace("\n", " ")
|
||||
desc_str = f" {desc}" if desc else ""
|
||||
print(f" ⬜ #{t['id']} {t['title'][:50]}{label_str}{desc_str}")
|
||||
|
||||
if done_tasks:
|
||||
# 최근 완료 5건만 표시
|
||||
recent_done = sorted(done_tasks, key=lambda t: t.get("done_at", ""), reverse=True)[:5]
|
||||
print(f" 최근 완료 (최대 5건):")
|
||||
for t in recent_done:
|
||||
print(f" ✅ #{t['id']} {t['title'][:50]}")
|
||||
except Exception as e:
|
||||
print(f" 태스크 조회 실패: {e}")
|
||||
|
||||
# 3) Git log (현재 디렉토리 기준)
|
||||
print(f"\n[최근 커밋 5건]")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "log", "--oneline", "-5"],
|
||||
capture_output=True, timeout=10,
|
||||
encoding="utf-8", errors="replace",
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
for line in result.stdout.strip().splitlines():
|
||||
print(f" {line}")
|
||||
else:
|
||||
print(" (git log 없음)")
|
||||
except Exception:
|
||||
print(" (git 실행 불가)")
|
||||
|
||||
# 4) Devlog (오늘/어제)
|
||||
print(f"\n[Devlog]")
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||||
|
||||
devlog_found = False
|
||||
for date_str in [today, yesterday]:
|
||||
devlog_path = Path("docs/devlog") / f"{date_str}.md"
|
||||
if devlog_path.exists():
|
||||
content = devlog_path.read_text(encoding="utf-8").strip()
|
||||
# 최대 500자
|
||||
if len(content) > 500:
|
||||
content = content[:500] + "\n ...(생략)"
|
||||
print(f" [{date_str}]")
|
||||
for line in content.splitlines():
|
||||
print(f" {line}")
|
||||
devlog_found = True
|
||||
break
|
||||
|
||||
if not devlog_found:
|
||||
print(" (최근 devlog 없음)")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(__doc__)
|
||||
@@ -208,6 +314,11 @@ def main():
|
||||
print("Error: title is required")
|
||||
return
|
||||
create_task(title, desc, done=is_done, labels=labels)
|
||||
elif cmd == "projects":
|
||||
list_projects()
|
||||
elif cmd == "report":
|
||||
pid = int(sys.argv[2]) if len(sys.argv) > 2 else None
|
||||
report(pid)
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
print(__doc__)
|
||||
|
||||
Reference in New Issue
Block a user