worklog-for-claude 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ .env
2
+ node_modules/
3
+ __pycache__/
4
+ *.pyc
5
+ .DS_Store
6
+ *.bak
7
+ .worklogs/
8
+ .version
9
+ .version-checked
10
+ .claude/
11
+ update.sh
12
+
13
+ # --- ai-bouncer start ---
14
+ # ai-bouncer runtime artifacts
15
+ .claude/ai-bouncer/.version-checked
16
+ .claude/ai-bouncer/config.json
17
+ .claude/ai-bouncer/manifest.json
18
+ .claude/**/*.backup-*
19
+ .claude/settings.json
20
+ # ai-bouncer installed files
21
+ .claude/agents/dev.md
22
+ .claude/agents/intent.md
23
+ .claude/agents/lead.md
24
+ .claude/agents/qa.md
25
+ .claude/agents/verifier.md
26
+ .claude/agents/guides/tc-guide.md
27
+ .claude/skills/dev-bounce/SKILL.md
28
+ .claude/skills/update-bouncer/SKILL.md
29
+ .claude/hooks/hooks.json
30
+ .claude/hooks/plan-gate.sh
31
+ .claude/hooks/bash-gate.sh
32
+ .claude/hooks/doc-reminder.sh
33
+ .claude/hooks/bash-audit.sh
34
+ .claude/hooks/completion-gate.sh
35
+ .claude/hooks/stop-active-cleanup.sh
36
+ .claude/hooks/subagent-track.sh
37
+ .claude/hooks/subagent-cleanup.sh
38
+ .claude/hooks/lib/resolve-task.sh
39
+ .claude/scripts/update-check.sh
40
+ update.sh
41
+ uninstall.sh
42
+ # --- ai-bouncer end ---
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: worklog-for-claude
3
+ Version: 0.1.0
4
+ Summary: MCP server for worklog and project documentation management
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.28.1
7
+ Requires-Dist: mcp[cli]>=1.21.0
8
+ Provides-Extra: dev
9
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
10
+ Requires-Dist: pytest>=7.0; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # worklog-mcp
14
+
15
+ **The project doc that writes itself.**
16
+
17
+ MCP server that manages worklogs and keeps `PROJECT.md` up to date — across Claude Code, Cursor, and Claude Desktop.
18
+
19
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../LICENSE)
20
+ [![MCP](https://img.shields.io/badge/MCP-compatible-blue)](https://modelcontextprotocol.io)
21
+ [![Python](https://img.shields.io/badge/Python-3.10+-green)](https://python.org)
22
+
23
+ ---
24
+
25
+ ## What It Does
26
+
27
+ - **Worklog** — records what you worked on into `.worklogs/YYYY-MM-DD.md`, optionally syncs to Notion
28
+ - **Project doc** — creates and maintains `PROJECT.md` with structure, decisions, and solved problems
29
+ - **Gap detection** — compares recent git commits to `PROJECT.md` and surfaces what's missing
30
+ - **Any client** — Claude Code, Cursor, Claude Desktop, anything MCP-compatible
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ git clone https://github.com/kangraemin/worklog-for-claude
36
+ cd worklog-for-claude/mcp
37
+ uv sync
38
+ ```
39
+
40
+ ## Connect
41
+
42
+ Add to your MCP client config. Replace the path with the absolute path to this `mcp/` directory.
43
+
44
+ **Claude Code** — `.claude/settings.json`:
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "worklog-mcp": {
49
+ "command": "uv",
50
+ "args": ["--directory", "/path/to/worklog-for-claude/mcp", "run", "worklog-mcp"]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ **Cursor** — `~/.cursor/mcp.json` (same format)
57
+
58
+ **Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json` (same format)
59
+
60
+ See `examples/` for complete config files.
61
+
62
+ ## Tools
63
+
64
+ | Tool | Description |
65
+ |---|---|
66
+ | `write_worklog` | Append an entry to `.worklogs/YYYY-MM-DD.md` |
67
+ | `read_worklog` | Read worklog for a given date (default: today) |
68
+ | `write_worklog_to_notion` | Send worklog entry to Notion DB |
69
+ | `read_project_doc` | Read `PROJECT.md` with section parsing |
70
+ | `create_project_doc` | Create `PROJECT.md` with 7 standard sections |
71
+ | `analyze_gaps` | Compare recent git commits to `PROJECT.md`, return gaps |
72
+ | `update_project_doc` | Update a specific section (replace or append) |
73
+
74
+ ## PROJECT.md Sections
75
+
76
+ ```
77
+ ## 이게 뭔가 — one-line description
78
+ ## 왜 만들었나 — motivation and problem
79
+ ## 구조 — folder/file structure
80
+ ## 기술 스택 — tech choices and reasons
81
+ ## 주요 결정들 — key architectural decisions
82
+ ## 해결한 문제들 — bugs and how they were fixed
83
+ ## 지금 상태 — current state, what works, what's next
84
+ ```
85
+
86
+ ## Notion Setup
87
+
88
+ Set environment variables before running the server:
89
+
90
+ ```bash
91
+ export NOTION_TOKEN=secret_...
92
+ export NOTION_DB_ID=your-db-id
93
+ ```
94
+
95
+ Or pass them directly as tool arguments.
96
+
97
+ Required Notion DB columns: `Title`, `Project`, `Cost`, `Duration`, `Model`, `Tokens`, `DateTime`
98
+
99
+ ## Gap Detection
100
+
101
+ `analyze_gaps` watches your git history and finds what's missing in `PROJECT.md`:
102
+
103
+ - `feat:` commits → checks `주요 결정들`, `지금 상태`
104
+ - `fix:` commits → checks `해결한 문제들`
105
+ - `refactor:` commits → checks `구조`, `기술 스택`
106
+ - Empty sections → always flagged
107
+ - Recent `docs:` or `PROJECT.md` commit → skips gap check
108
+
109
+ ## Test
110
+
111
+ ```bash
112
+ uv run pytest tests/ -v
113
+ ```
114
+
115
+ 71 tests, all passing.
@@ -0,0 +1,103 @@
1
+ # worklog-mcp
2
+
3
+ **The project doc that writes itself.**
4
+
5
+ MCP server that manages worklogs and keeps `PROJECT.md` up to date — across Claude Code, Cursor, and Claude Desktop.
6
+
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../LICENSE)
8
+ [![MCP](https://img.shields.io/badge/MCP-compatible-blue)](https://modelcontextprotocol.io)
9
+ [![Python](https://img.shields.io/badge/Python-3.10+-green)](https://python.org)
10
+
11
+ ---
12
+
13
+ ## What It Does
14
+
15
+ - **Worklog** — records what you worked on into `.worklogs/YYYY-MM-DD.md`, optionally syncs to Notion
16
+ - **Project doc** — creates and maintains `PROJECT.md` with structure, decisions, and solved problems
17
+ - **Gap detection** — compares recent git commits to `PROJECT.md` and surfaces what's missing
18
+ - **Any client** — Claude Code, Cursor, Claude Desktop, anything MCP-compatible
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ git clone https://github.com/kangraemin/worklog-for-claude
24
+ cd worklog-for-claude/mcp
25
+ uv sync
26
+ ```
27
+
28
+ ## Connect
29
+
30
+ Add to your MCP client config. Replace the path with the absolute path to this `mcp/` directory.
31
+
32
+ **Claude Code** — `.claude/settings.json`:
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "worklog-mcp": {
37
+ "command": "uv",
38
+ "args": ["--directory", "/path/to/worklog-for-claude/mcp", "run", "worklog-mcp"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ **Cursor** — `~/.cursor/mcp.json` (same format)
45
+
46
+ **Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json` (same format)
47
+
48
+ See `examples/` for complete config files.
49
+
50
+ ## Tools
51
+
52
+ | Tool | Description |
53
+ |---|---|
54
+ | `write_worklog` | Append an entry to `.worklogs/YYYY-MM-DD.md` |
55
+ | `read_worklog` | Read worklog for a given date (default: today) |
56
+ | `write_worklog_to_notion` | Send worklog entry to Notion DB |
57
+ | `read_project_doc` | Read `PROJECT.md` with section parsing |
58
+ | `create_project_doc` | Create `PROJECT.md` with 7 standard sections |
59
+ | `analyze_gaps` | Compare recent git commits to `PROJECT.md`, return gaps |
60
+ | `update_project_doc` | Update a specific section (replace or append) |
61
+
62
+ ## PROJECT.md Sections
63
+
64
+ ```
65
+ ## 이게 뭔가 — one-line description
66
+ ## 왜 만들었나 — motivation and problem
67
+ ## 구조 — folder/file structure
68
+ ## 기술 스택 — tech choices and reasons
69
+ ## 주요 결정들 — key architectural decisions
70
+ ## 해결한 문제들 — bugs and how they were fixed
71
+ ## 지금 상태 — current state, what works, what's next
72
+ ```
73
+
74
+ ## Notion Setup
75
+
76
+ Set environment variables before running the server:
77
+
78
+ ```bash
79
+ export NOTION_TOKEN=secret_...
80
+ export NOTION_DB_ID=your-db-id
81
+ ```
82
+
83
+ Or pass them directly as tool arguments.
84
+
85
+ Required Notion DB columns: `Title`, `Project`, `Cost`, `Duration`, `Model`, `Tokens`, `DateTime`
86
+
87
+ ## Gap Detection
88
+
89
+ `analyze_gaps` watches your git history and finds what's missing in `PROJECT.md`:
90
+
91
+ - `feat:` commits → checks `주요 결정들`, `지금 상태`
92
+ - `fix:` commits → checks `해결한 문제들`
93
+ - `refactor:` commits → checks `구조`, `기술 스택`
94
+ - Empty sections → always flagged
95
+ - Recent `docs:` or `PROJECT.md` commit → skips gap check
96
+
97
+ ## Test
98
+
99
+ ```bash
100
+ uv run pytest tests/ -v
101
+ ```
102
+
103
+ 71 tests, all passing.
@@ -0,0 +1,17 @@
1
+ {
2
+ "mcpServers": {
3
+ "worklog-mcp": {
4
+ "command": "uv",
5
+ "args": [
6
+ "--directory",
7
+ "/ABSOLUTE/PATH/TO/worklog-for-claude/mcp",
8
+ "run",
9
+ "worklog-mcp"
10
+ ],
11
+ "env": {
12
+ "NOTION_TOKEN": "secret_...",
13
+ "NOTION_DB_ID": "your-notion-db-id"
14
+ }
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "mcpServers": {
3
+ "worklog-mcp": {
4
+ "command": "uv",
5
+ "args": [
6
+ "--directory",
7
+ "/ABSOLUTE/PATH/TO/worklog-for-claude/mcp",
8
+ "run",
9
+ "worklog-mcp"
10
+ ],
11
+ "env": {
12
+ "NOTION_TOKEN": "secret_...",
13
+ "NOTION_DB_ID": "your-notion-db-id"
14
+ }
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "mcpServers": {
3
+ "worklog-mcp": {
4
+ "command": "uv",
5
+ "args": [
6
+ "--directory",
7
+ "/ABSOLUTE/PATH/TO/worklog-for-claude/mcp",
8
+ "run",
9
+ "worklog-mcp"
10
+ ],
11
+ "env": {
12
+ "NOTION_TOKEN": "secret_...",
13
+ "NOTION_DB_ID": "your-notion-db-id"
14
+ }
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "worklog-for-claude"
7
+ version = "0.1.0"
8
+ description = "MCP server for worklog and project documentation management"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "httpx>=0.28.1",
13
+ "mcp[cli]>=1.21.0",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ dev = [
18
+ "pytest>=7.0",
19
+ "pytest-asyncio>=0.23",
20
+ ]
21
+
22
+ [project.scripts]
23
+ worklog-for-claude = "worklog_mcp.server:main"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/worklog_mcp"]
27
+
28
+ [tool.pytest.ini_options]
29
+ asyncio_mode = "auto"
30
+ testpaths = ["tests"]
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=9.0.2",
35
+ "pytest-asyncio>=1.3.0",
36
+ "pytest-httpx>=0.36.0",
37
+ ]
File without changes
@@ -0,0 +1,39 @@
1
+ from mcp.server.fastmcp import FastMCP
2
+ from worklog_mcp.tools.worklog import write_worklog, read_worklog
3
+ from worklog_mcp.tools.project_doc import (
4
+ read_project_doc,
5
+ create_project_doc,
6
+ analyze_gaps,
7
+ update_project_doc,
8
+ )
9
+ from worklog_mcp.tools.notion import write_worklog_to_notion
10
+ from worklog_mcp.utils.git import get_commits_since_file_update
11
+
12
+
13
+ def get_commits_since_update(project_path: str = ".") -> int:
14
+ """PROJECT.md 마지막 수정 이후 커밋 수 반환. hook에서 빠르게 호출하는 용도.
15
+
16
+ Args:
17
+ project_path: 프로젝트 루트 디렉토리 경로 (기본값: 현재 디렉토리)
18
+ """
19
+ return get_commits_since_file_update(project_path, "PROJECT.md")
20
+
21
+
22
+ mcp = FastMCP("worklog-mcp")
23
+
24
+ mcp.tool()(write_worklog)
25
+ mcp.tool()(read_worklog)
26
+ mcp.tool()(read_project_doc)
27
+ mcp.tool()(create_project_doc)
28
+ mcp.tool()(analyze_gaps)
29
+ mcp.tool()(update_project_doc)
30
+ mcp.tool()(write_worklog_to_notion)
31
+ mcp.tool()(get_commits_since_update)
32
+
33
+
34
+ def main():
35
+ mcp.run(transport="stdio")
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
@@ -0,0 +1,104 @@
1
+ import os
2
+ from datetime import datetime
3
+
4
+ import httpx
5
+
6
+
7
+ def _md_to_notion_blocks(content: str) -> list[dict]:
8
+ """마크다운 텍스트를 Notion block 리스트로 변환."""
9
+ blocks = []
10
+ for line in content.splitlines():
11
+ stripped = line.strip()
12
+ if not stripped:
13
+ continue
14
+ text = stripped[:2000]
15
+ if stripped.startswith("### "):
16
+ blocks.append({"object": "block", "type": "heading_3",
17
+ "heading_3": {"rich_text": [{"text": {"content": text[4:]}}]}})
18
+ elif stripped.startswith("## "):
19
+ blocks.append({"object": "block", "type": "heading_2",
20
+ "heading_2": {"rich_text": [{"text": {"content": text[3:]}}]}})
21
+ elif stripped.startswith("# "):
22
+ blocks.append({"object": "block", "type": "heading_1",
23
+ "heading_1": {"rich_text": [{"text": {"content": text[2:]}}]}})
24
+ elif stripped.startswith("- "):
25
+ blocks.append({"object": "block", "type": "bulleted_list_item",
26
+ "bulleted_list_item": {"rich_text": [{"text": {"content": text[2:]}}]}})
27
+ else:
28
+ blocks.append({"object": "block", "type": "paragraph",
29
+ "paragraph": {"rich_text": [{"text": {"content": text}}]}})
30
+ return blocks
31
+
32
+
33
+ def write_worklog_to_notion(
34
+ title: str,
35
+ content: str,
36
+ project: str,
37
+ notion_token: str = "",
38
+ notion_db_id: str = "",
39
+ cost: float = 0.0,
40
+ duration: int = 0,
41
+ model: str = "claude-sonnet-4-6",
42
+ tokens: int = 0,
43
+ ) -> str:
44
+ """워크로그를 Notion DB에 기록한다.
45
+
46
+ Args:
47
+ title: 작업 제목 (Notion 페이지 제목)
48
+ content: 워크로그 내용 (마크다운)
49
+ project: 프로젝트 이름
50
+ notion_token: Notion API 토큰. 미지정 시 NOTION_TOKEN 환경변수 사용
51
+ notion_db_id: Notion DB ID. 미지정 시 NOTION_DB_ID 환경변수 사용
52
+ cost: 이번 작업 비용 (USD)
53
+ duration: 소요 시간 (분)
54
+ model: 사용한 모델
55
+ tokens: 사용한 토큰 수
56
+ """
57
+ token = notion_token or os.environ.get("NOTION_TOKEN", "")
58
+ db_id = notion_db_id or os.environ.get("NOTION_DB_ID", "")
59
+
60
+ if not token:
61
+ raise ValueError("NOTION_TOKEN이 필요합니다. 파라미터 또는 환경변수로 설정하세요.")
62
+ if not db_id:
63
+ raise ValueError("NOTION_DB_ID가 필요합니다. 파라미터 또는 환경변수로 설정하세요.")
64
+
65
+ now = datetime.now().isoformat()
66
+ blocks = _md_to_notion_blocks(content)
67
+
68
+ payload: dict = {
69
+ "parent": {"database_id": db_id},
70
+ "icon": {"type": "emoji", "emoji": "📖"},
71
+ "properties": {
72
+ "Title": {"title": [{"text": {"content": title}}]},
73
+ "Project": {"select": {"name": project}},
74
+ "Model": {"select": {"name": model}},
75
+ "DateTime": {"date": {"start": now}},
76
+ },
77
+ "children": blocks,
78
+ }
79
+
80
+ if cost:
81
+ payload["properties"]["Cost"] = {"number": round(cost, 3)}
82
+ if duration:
83
+ payload["properties"]["Duration"] = {"number": duration}
84
+ if tokens:
85
+ payload["properties"]["Tokens"] = {"number": tokens}
86
+
87
+ try:
88
+ response = httpx.post(
89
+ "https://api.notion.com/v1/pages",
90
+ headers={
91
+ "Authorization": f"Bearer {token}",
92
+ "Notion-Version": "2022-06-28",
93
+ "Content-Type": "application/json",
94
+ },
95
+ json=payload,
96
+ timeout=30.0,
97
+ )
98
+ except httpx.TimeoutException:
99
+ raise RuntimeError("Notion API 타임아웃 (30초)")
100
+
101
+ if response.status_code == 200:
102
+ return "OK"
103
+
104
+ raise RuntimeError(f"Notion API 실패: HTTP {response.status_code} — {response.text[:200]}")
@@ -0,0 +1,164 @@
1
+ import re
2
+ from pathlib import Path
3
+ from worklog_mcp.utils.git import get_diff, get_commits_since_file_update
4
+
5
+ SECTIONS = ["이게 뭔가", "왜 만들었나", "구조", "기술 스택", "주요 결정들", "해결한 문제들", "지금 상태"]
6
+
7
+
8
+ def _parse_sections(content: str) -> dict[str, str]:
9
+ """PROJECT.md 내용을 섹션별로 파싱."""
10
+ sections: dict[str, str] = {s: "" for s in SECTIONS}
11
+ current = None
12
+ lines: list[str] = []
13
+
14
+ for line in content.splitlines():
15
+ m = re.match(r"^## (.+)$", line)
16
+ if m:
17
+ if current is not None:
18
+ sections[current] = "\n".join(lines).strip()
19
+ candidate = m.group(1).strip()
20
+ current = candidate if candidate in SECTIONS else None
21
+ lines = []
22
+ elif current is not None:
23
+ lines.append(line)
24
+
25
+ if current is not None:
26
+ sections[current] = "\n".join(lines).strip()
27
+
28
+ return sections
29
+
30
+
31
+ def read_project_doc(project_path: str) -> dict:
32
+ """PROJECT.md를 읽어 반환한다.
33
+
34
+ Args:
35
+ project_path: 프로젝트 루트 디렉토리 경로
36
+ """
37
+ path = Path(project_path).resolve()
38
+ if not path.exists():
39
+ raise FileNotFoundError(f"Path not found: {project_path}")
40
+
41
+ doc_path = path / "PROJECT.md"
42
+ if not doc_path.exists():
43
+ return {"exists": False, "content": "", "sections": {}}
44
+
45
+ content = doc_path.read_text(encoding="utf-8")
46
+ return {
47
+ "exists": True,
48
+ "content": content,
49
+ "sections": _parse_sections(content),
50
+ }
51
+
52
+
53
+ def create_project_doc(project_path: str, sections: dict[str, str]) -> str:
54
+ """PROJECT.md를 생성한다. 이미 존재하면 에러.
55
+
56
+ Args:
57
+ project_path: 프로젝트 루트 디렉토리 경로
58
+ sections: 섹션별 초기 내용 (없으면 빈 섹션)
59
+ """
60
+ path = Path(project_path).resolve()
61
+ if not path.exists():
62
+ raise FileNotFoundError(f"Path not found: {project_path}")
63
+
64
+ doc_path = path / "PROJECT.md"
65
+ if doc_path.exists():
66
+ raise FileExistsError(f"PROJECT.md already exists at {doc_path}")
67
+
68
+ project_name = path.name
69
+ lines = [f"# {project_name}\n"]
70
+ for section in SECTIONS:
71
+ lines.append(f"\n## {section}\n")
72
+ content = sections.get(section, "")
73
+ if content:
74
+ lines.append(f"{content}\n")
75
+
76
+ doc_path.write_text("".join(lines), encoding="utf-8")
77
+ return f"Created {doc_path}"
78
+
79
+
80
+ def analyze_gaps(project_path: str, n: int = 10) -> dict:
81
+ """PROJECT.md와 최근 변경사항을 반환한다. Claude가 gap을 판단한다.
82
+
83
+ Args:
84
+ project_path: 프로젝트 루트 디렉토리 경로
85
+ n: 분석할 최근 커밋 수 (기본 10)
86
+
87
+ Returns:
88
+ {
89
+ "project_doc": str | None, # 현재 PROJECT.md 내용 (없으면 None)
90
+ "sections": dict[str, str], # 섹션별 파싱 결과 (없으면 {})
91
+ "diff_mode": "full" | "summary",
92
+ "diff": str,
93
+ "changed_files": list[str],
94
+ "commits": list[str],
95
+ "line_count": int,
96
+ "commits_since_doc_update": int, # PROJECT.md 마지막 수정 이후 커밋 수
97
+ }
98
+ """
99
+ path = Path(project_path).resolve()
100
+
101
+ diff_info = get_diff(str(path), n=n)
102
+
103
+ doc_path = path / "PROJECT.md"
104
+ if doc_path.exists():
105
+ project_doc = doc_path.read_text(encoding="utf-8")
106
+ sections = _parse_sections(project_doc)
107
+ else:
108
+ project_doc = None
109
+ sections = {}
110
+
111
+ commits_since = get_commits_since_file_update(str(path), "PROJECT.md")
112
+
113
+ return {
114
+ "project_doc": project_doc,
115
+ "sections": sections,
116
+ "diff_mode": diff_info["mode"],
117
+ "diff": diff_info["diff"],
118
+ "changed_files": diff_info["changed_files"],
119
+ "commits": diff_info["commits"],
120
+ "line_count": diff_info["line_count"],
121
+ "commits_since_doc_update": commits_since,
122
+ }
123
+
124
+
125
+ def update_project_doc(
126
+ project_path: str, section: str, content: str, append: bool = False
127
+ ) -> str:
128
+ """PROJECT.md의 특정 섹션을 업데이트한다.
129
+
130
+ Args:
131
+ project_path: 프로젝트 루트 디렉토리 경로
132
+ section: 업데이트할 섹션명 (예: "이게 뭔가")
133
+ content: 새 내용
134
+ append: True면 기존 내용에 추가, False면 교체
135
+ """
136
+ path = Path(project_path).resolve()
137
+ doc_path = path / "PROJECT.md"
138
+
139
+ if not doc_path.exists():
140
+ raise FileNotFoundError(f"PROJECT.md not found at {doc_path}")
141
+
142
+ if section not in SECTIONS:
143
+ raise ValueError(f"Invalid section: '{section}'. Must be one of: {SECTIONS}")
144
+
145
+ full_content = doc_path.read_text(encoding="utf-8")
146
+ parsed = _parse_sections(full_content)
147
+
148
+ if append and parsed.get(section):
149
+ parsed[section] = parsed[section] + "\n" + content
150
+ else:
151
+ parsed[section] = content
152
+
153
+ # 재조립: 헤더(# 프로젝트명) 유지
154
+ header_match = re.match(r"^(#[^\n]*\n)", full_content)
155
+ header = header_match.group(1) if header_match else ""
156
+
157
+ lines = [header] if header else []
158
+ for s in SECTIONS:
159
+ lines.append(f"\n## {s}\n")
160
+ if parsed.get(s):
161
+ lines.append(f"{parsed[s]}\n")
162
+
163
+ doc_path.write_text("".join(lines), encoding="utf-8")
164
+ return f"Updated section '{section}' in {doc_path}"