Files
Allen930311 9de71654fc feat: add unified-diary skill (#246)
* 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
2026-03-09 11:52:09 +01:00

245 lines
9.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)