* feat: add unified-diary skill * chore: refine diary skill metadata for quality bar compliance * fix: address PR feedback - fix NameError, Obsidian path, and add missing script
245 lines
9.3 KiB
Python
245 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
AI Agent Context Preparer v2
|
||
Usage: python prepare_context.py [directory_path]
|
||
Generates a standardized AGENT_CONTEXT.md with 5 core sections:
|
||
1. 專案目標 (Project Goal) - from README
|
||
2. 技術棧與環境 (Tech Stack) - from config files
|
||
3. 核心目錄結構 (Core Structure) - recursive tree
|
||
4. 架構與設計約定 (Conventions) - from L1 cache
|
||
5. 目前進度與待辦 (Status & TODO) - from latest diary
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import glob
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
|
||
|
||
def get_tree(path, prefix="", max_depth=3, current_depth=0):
|
||
"""Recursive directory tree generator with depth limit."""
|
||
if current_depth >= max_depth:
|
||
return []
|
||
try:
|
||
entries = sorted(os.listdir(path))
|
||
except PermissionError:
|
||
return []
|
||
tree_lines = []
|
||
skip_prefixes = (".", "node_modules", "__pycache__", "dist", "build", "venv", ".git")
|
||
filtered = [e for e in entries if not e.startswith(skip_prefixes)]
|
||
for i, entry in enumerate(filtered):
|
||
is_last = i == len(filtered) - 1
|
||
connector = "└── " if is_last else "├── "
|
||
full_path = os.path.join(path, entry)
|
||
tree_lines.append(f"{prefix}{connector}{entry}")
|
||
if os.path.isdir(full_path):
|
||
extension = " " if is_last else "│ "
|
||
tree_lines.extend(get_tree(full_path, prefix + extension, max_depth, current_depth + 1))
|
||
return tree_lines
|
||
|
||
|
||
def extract_readme_summary(root):
|
||
"""Extract first meaningful paragraph from README as project goal."""
|
||
readme = root / "README.md"
|
||
if not readme.exists():
|
||
return None
|
||
text = readme.read_text(encoding="utf-8", errors="ignore")
|
||
lines = text.strip().split("\n")
|
||
# Skip title lines (# heading) and blank lines, grab first paragraph
|
||
summary_lines = []
|
||
found_content = False
|
||
for line in lines:
|
||
stripped = line.strip()
|
||
if not found_content:
|
||
if stripped and not stripped.startswith("#"):
|
||
found_content = True
|
||
summary_lines.append(stripped)
|
||
else:
|
||
if stripped == "" and summary_lines:
|
||
break
|
||
summary_lines.append(stripped)
|
||
return " ".join(summary_lines) if summary_lines else None
|
||
|
||
|
||
def extract_tech_stack(root):
|
||
"""Extract tech stack info from config files."""
|
||
stack_info = []
|
||
|
||
# package.json
|
||
pkg = root / "package.json"
|
||
if pkg.exists():
|
||
try:
|
||
data = json.loads(pkg.read_text(encoding="utf-8"))
|
||
deps = list(data.get("dependencies", {}).keys())
|
||
dev_deps = list(data.get("devDependencies", {}).keys())
|
||
if deps:
|
||
stack_info.append(f"* **核心套件**:{', '.join(deps[:10])}")
|
||
if dev_deps:
|
||
stack_info.append(f"* **開發套件**:{', '.join(dev_deps[:8])}")
|
||
if "scripts" in data:
|
||
scripts = list(data["scripts"].keys())
|
||
stack_info.append(f"* **可用指令**:{', '.join(scripts)}")
|
||
except (json.JSONDecodeError, KeyError):
|
||
pass
|
||
|
||
# pyproject.toml - basic extraction
|
||
pyproject = root / "pyproject.toml"
|
||
if pyproject.exists():
|
||
text = pyproject.read_text(encoding="utf-8", errors="ignore")
|
||
stack_info.append(f"* **Python 專案**:使用 pyproject.toml 管理")
|
||
# Simple dependency extraction
|
||
if "dependencies" in text:
|
||
stack_info.append("* _詳見 pyproject.toml 的 dependencies 區塊_")
|
||
|
||
# requirements.txt
|
||
reqs = root / "requirements.txt"
|
||
if reqs.exists():
|
||
req_lines = [l.strip().split("==")[0].split(">=")[0]
|
||
for l in reqs.read_text(encoding="utf-8", errors="ignore").strip().split("\n")
|
||
if l.strip() and not l.startswith("#")]
|
||
if req_lines:
|
||
stack_info.append(f"* **Python 套件**:{', '.join(req_lines[:10])}")
|
||
|
||
return stack_info
|
||
|
||
|
||
def extract_latest_diary_todos(root):
|
||
"""Find the latest diary file and extract Next Steps / TODO items."""
|
||
# Search common diary locations
|
||
diary_dirs = [
|
||
root / "diary",
|
||
Path(os.path.expanduser("~")) / ".gemini" / "antigravity" / "global_skills" / "auto-skill" / "diary",
|
||
]
|
||
|
||
latest_file = None
|
||
latest_date = ""
|
||
|
||
for diary_dir in diary_dirs:
|
||
if not diary_dir.exists():
|
||
continue
|
||
# Glob for markdown files recursively
|
||
for md_file in diary_dir.rglob("*.md"):
|
||
name = md_file.stem
|
||
# Try to extract date from filename (YYYY-MM-DD format)
|
||
if len(name) >= 10 and name[:4].isdigit():
|
||
date_str = name[:10]
|
||
if date_str > latest_date:
|
||
latest_date = date_str
|
||
latest_file = md_file
|
||
|
||
if not latest_file:
|
||
return None, []
|
||
|
||
text = latest_file.read_text(encoding="utf-8", errors="ignore")
|
||
lines = text.split("\n")
|
||
|
||
todos = []
|
||
in_next_section = False
|
||
for line in lines:
|
||
stripped = line.strip()
|
||
# Detect "Next Steps" or "下一步" sections
|
||
if any(kw in stripped.lower() for kw in ["next step", "下一步", "next steps", "待辦", "todo"]):
|
||
in_next_section = True
|
||
continue
|
||
if in_next_section:
|
||
if stripped.startswith("- [") or stripped.startswith("* ["):
|
||
todos.append(stripped)
|
||
elif stripped.startswith("#"):
|
||
break # Next section header, stop
|
||
|
||
return latest_date, todos
|
||
|
||
|
||
def prepare_context(root_path):
|
||
root = Path(root_path).resolve()
|
||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||
print(f"📋 Preparing context for: {root}")
|
||
|
||
context_file = root / "AGENT_CONTEXT.md"
|
||
|
||
with open(context_file, "w", encoding="utf-8") as f:
|
||
# Header
|
||
f.write(f"# 專案上下文 (Agent Context):{root.name}\n\n")
|
||
f.write(f"> **最後更新時間**:{now}\n")
|
||
f.write(f"> **自動生成**:由 `prepare_context.py` 產生,供 AI Agent 快速掌握專案全局\n\n")
|
||
f.write("---\n\n")
|
||
|
||
# Section 1: 專案目標
|
||
f.write("## 🎯 1. 專案目標 (Project Goal)\n")
|
||
readme_summary = extract_readme_summary(root)
|
||
if readme_summary:
|
||
f.write(f"* **核心目的**:{readme_summary}\n")
|
||
else:
|
||
f.write("* **核心目的**:_(請手動補充,或建立 README.md)_\n")
|
||
readme = root / "README.md"
|
||
if readme.exists():
|
||
f.write(f"* _完整說明見 [README.md](README.md)_\n")
|
||
f.write("\n")
|
||
|
||
# Section 2: 技術棧與環境
|
||
f.write("## 🛠️ 2. 技術棧與環境 (Tech Stack & Environment)\n")
|
||
stack_info = extract_tech_stack(root)
|
||
if stack_info:
|
||
f.write("\n".join(stack_info))
|
||
f.write("\n")
|
||
else:
|
||
f.write("* _(未偵測到 package.json / pyproject.toml / requirements.txt)_\n")
|
||
|
||
# Also include raw config snippets for AI reference
|
||
config_files = ["package.json", "pyproject.toml", "requirements.txt", ".env.example", "clasp.json"]
|
||
has_config = False
|
||
for cfg in config_files:
|
||
cfg_path = root / cfg
|
||
if cfg_path.exists():
|
||
if not has_config:
|
||
f.write("\n### 原始設定檔\n")
|
||
has_config = True
|
||
ext = cfg.split(".")[-1]
|
||
lang_map = {"json": "json", "toml": "toml", "txt": "text", "example": "text"}
|
||
lang = lang_map.get(ext, "text")
|
||
content = cfg_path.read_text(encoding="utf-8", errors="ignore")
|
||
# Truncate very long config files
|
||
if len(content) > 3000:
|
||
content = content[:3000] + "\n... (truncated)"
|
||
f.write(f"\n<details><summary>{cfg}</summary>\n\n```{lang}\n{content}\n```\n</details>\n")
|
||
f.write("\n")
|
||
|
||
# Section 3: 核心目錄結構
|
||
f.write("## 📂 3. 核心目錄結構 (Core Structure)\n")
|
||
f.write("_(💡 AI 讀取守則:請依據此結構尋找對應檔案,勿盲目猜測路徑)_\n")
|
||
f.write("```text\n")
|
||
f.write(f"{root.name}/\n")
|
||
f.write("\n".join(get_tree(root)))
|
||
f.write("\n```\n\n")
|
||
|
||
# Section 4: 架構與設計約定
|
||
f.write("## 🏛️ 4. 架構與設計約定 (Architecture & Conventions)\n")
|
||
local_exp = root / ".auto-skill-local.md"
|
||
if local_exp.exists():
|
||
f.write("_(來自專案 L1 快取 `.auto-skill-local.md`)_\n\n")
|
||
f.write(local_exp.read_text(encoding="utf-8", errors="ignore"))
|
||
f.write("\n\n")
|
||
else:
|
||
f.write("* _(尚無 `.auto-skill-local.md`,專案踩坑經驗將在開發過程中自動累積)_\n\n")
|
||
|
||
# Section 5: 目前進度與待辦
|
||
f.write("## 🚦 5. 目前進度與待辦 (Current Status & TODO)\n")
|
||
latest_date, todos = extract_latest_diary_todos(root)
|
||
if todos:
|
||
f.write(f"_(自動提取自最近日記 {latest_date})_\n\n")
|
||
f.write("### 🚧 待辦事項\n")
|
||
for todo in todos:
|
||
f.write(f"{todo}\n")
|
||
f.write("\n")
|
||
else:
|
||
f.write("* _(尚無日記記錄,或日記中無「下一步」區塊)_\n\n")
|
||
|
||
print(f"✅ Created: {context_file}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
target = sys.argv[1] if len(sys.argv) > 1 else "."
|
||
prepare_context(target)
|