Skip to content

Commit 0a03bda

Browse files
solzipclaude
andcommitted
feat: install/uninstall CLI 명령 추가 — pip install 후 hook 자동 등록
pip install claude-diary → claude-diary install 한 방에 ~/.claude/settings.json에 Stop hook이 등록되도록 구현 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 57cd6b2 commit 0a03bda

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

src/claude_diary/cli/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from claude_diary.cli.config import cmd_config, cmd_init, cmd_migrate, _add_exporter_interactive
1919
from claude_diary.cli.team import cmd_team
2020
from claude_diary.cli.maintenance import cmd_reindex, cmd_audit, cmd_delete, cmd_dashboard
21+
from claude_diary.cli.setup import cmd_install, cmd_uninstall
2122

2223

2324
def main():
@@ -104,6 +105,10 @@ def main():
104105
p_dashboard.add_argument("--port", type=int, default=8787, help="Server port (default: 8787)")
105106
p_dashboard.add_argument("--months", type=int, default=3, help="Months of data (default: 3)")
106107

108+
# install / uninstall
109+
sub.add_parser("install", help="Register claude-diary hook in Claude Code")
110+
sub.add_parser("uninstall", help="Remove claude-diary hook from Claude Code")
111+
107112
args = parser.parse_args()
108113

109114
if args.command is None:
@@ -124,6 +129,8 @@ def main():
124129
"audit": cmd_audit,
125130
"delete": cmd_delete,
126131
"dashboard": cmd_dashboard,
132+
"install": cmd_install,
133+
"uninstall": cmd_uninstall,
127134
}
128135

129136
fn = commands.get(args.command)

src/claude_diary/cli/setup.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Install/uninstall commands — register claude-diary hook in Claude Code settings."""
2+
3+
import json
4+
import os
5+
import sys
6+
7+
8+
HOOK_COMMAND = "PYTHONIOENCODING=utf-8 python -m claude_diary.hook"
9+
10+
HOOK_ENTRY = {
11+
"type": "command",
12+
"command": HOOK_COMMAND,
13+
}
14+
15+
16+
def _get_claude_settings_path():
17+
"""Return path to ~/.claude/settings.json."""
18+
home = os.path.expanduser("~")
19+
return os.path.join(home, ".claude", "settings.json")
20+
21+
22+
def _load_claude_settings(path):
23+
"""Load existing settings or return empty dict."""
24+
if os.path.exists(path):
25+
try:
26+
with open(path, "r", encoding="utf-8") as f:
27+
return json.load(f)
28+
except (json.JSONDecodeError, IOError):
29+
pass
30+
return {}
31+
32+
33+
def _save_claude_settings(path, settings):
34+
"""Save settings to file, creating directory if needed."""
35+
os.makedirs(os.path.dirname(path), exist_ok=True)
36+
with open(path, "w", encoding="utf-8") as f:
37+
json.dump(settings, f, indent=2, ensure_ascii=False)
38+
f.write("\n")
39+
40+
41+
def _is_diary_hook(hook):
42+
"""Check if a hook entry is a claude-diary hook."""
43+
command = hook.get("command", "")
44+
return "claude_diary.hook" in command
45+
46+
47+
def _find_existing_hook(settings):
48+
"""Check if claude-diary hook is already registered.
49+
Returns True if found.
50+
"""
51+
hooks = settings.get("hooks", {})
52+
stop_hooks = hooks.get("Stop", [])
53+
for group in stop_hooks:
54+
for hook in group.get("hooks", []):
55+
if _is_diary_hook(hook):
56+
return True
57+
return False
58+
59+
60+
def cmd_install(args):
61+
"""Register claude-diary Stop hook in ~/.claude/settings.json."""
62+
settings_path = _get_claude_settings_path()
63+
settings = _load_claude_settings(settings_path)
64+
65+
if _find_existing_hook(settings):
66+
print("claude-diary hook is already installed.")
67+
print(" Settings: %s" % settings_path)
68+
return
69+
70+
# Ensure hooks.Stop structure exists
71+
if "hooks" not in settings:
72+
settings["hooks"] = {}
73+
if "Stop" not in settings["hooks"]:
74+
settings["hooks"]["Stop"] = []
75+
76+
# Add hook
77+
settings["hooks"]["Stop"].append({
78+
"hooks": [HOOK_ENTRY],
79+
})
80+
81+
_save_claude_settings(settings_path, settings)
82+
83+
print("claude-diary hook installed successfully!")
84+
print()
85+
print(" Hook: %s" % HOOK_COMMAND)
86+
print(" Settings: %s" % settings_path)
87+
print()
88+
print("Every Claude Code session will now auto-generate a diary entry on exit.")
89+
90+
91+
def cmd_uninstall(args):
92+
"""Remove claude-diary Stop hook from ~/.claude/settings.json."""
93+
settings_path = _get_claude_settings_path()
94+
settings = _load_claude_settings(settings_path)
95+
96+
if not _find_existing_hook(settings):
97+
print("claude-diary hook is not installed.")
98+
return
99+
100+
# Remove diary hooks
101+
stop_hooks = settings.get("hooks", {}).get("Stop", [])
102+
new_stop = []
103+
for group in stop_hooks:
104+
remaining = [h for h in group.get("hooks", []) if not _is_diary_hook(h)]
105+
if remaining:
106+
new_stop.append({"hooks": remaining})
107+
108+
settings["hooks"]["Stop"] = new_stop
109+
110+
# Clean up empty structures
111+
if not settings["hooks"]["Stop"]:
112+
del settings["hooks"]["Stop"]
113+
if not settings["hooks"]:
114+
del settings["hooks"]
115+
116+
_save_claude_settings(settings_path, settings)
117+
118+
print("claude-diary hook uninstalled.")
119+
print(" Settings: %s" % settings_path)

0 commit comments

Comments
 (0)