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.
- worklog_for_claude-0.1.0/.gitignore +42 -0
- worklog_for_claude-0.1.0/PKG-INFO +115 -0
- worklog_for_claude-0.1.0/README.md +103 -0
- worklog_for_claude-0.1.0/examples/claude-code-settings.json +17 -0
- worklog_for_claude-0.1.0/examples/claude-desktop-config.json +17 -0
- worklog_for_claude-0.1.0/examples/cursor-mcp.json +17 -0
- worklog_for_claude-0.1.0/pyproject.toml +37 -0
- worklog_for_claude-0.1.0/src/worklog_mcp/__init__.py +0 -0
- worklog_for_claude-0.1.0/src/worklog_mcp/server.py +39 -0
- worklog_for_claude-0.1.0/src/worklog_mcp/tools/__init__.py +0 -0
- worklog_for_claude-0.1.0/src/worklog_mcp/tools/notion.py +104 -0
- worklog_for_claude-0.1.0/src/worklog_mcp/tools/project_doc.py +164 -0
- worklog_for_claude-0.1.0/src/worklog_mcp/tools/worklog.py +58 -0
- worklog_for_claude-0.1.0/src/worklog_mcp/utils/__init__.py +0 -0
- worklog_for_claude-0.1.0/src/worklog_mcp/utils/git.py +157 -0
- worklog_for_claude-0.1.0/tests/conftest.py +55 -0
- worklog_for_claude-0.1.0/tests/test_git_utils.py +80 -0
- worklog_for_claude-0.1.0/tests/test_install.py +34 -0
- worklog_for_claude-0.1.0/tests/test_notion.py +84 -0
- worklog_for_claude-0.1.0/tests/test_project_doc.py +338 -0
- worklog_for_claude-0.1.0/tests/test_server.py +55 -0
- worklog_for_claude-0.1.0/tests/test_worklog.py +138 -0
- worklog_for_claude-0.1.0/uv.lock +991 -0
|
@@ -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)
|
|
20
|
+
[](https://modelcontextprotocol.io)
|
|
21
|
+
[](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)
|
|
8
|
+
[](https://modelcontextprotocol.io)
|
|
9
|
+
[](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()
|
|
File without changes
|
|
@@ -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}"
|