agent-diary 4.3.0__py3-none-any.whl
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.
- agent_diary-4.3.0.dist-info/METADATA +599 -0
- agent_diary-4.3.0.dist-info/RECORD +59 -0
- agent_diary-4.3.0.dist-info/WHEEL +5 -0
- agent_diary-4.3.0.dist-info/entry_points.txt +4 -0
- agent_diary-4.3.0.dist-info/licenses/LICENSE +21 -0
- agent_diary-4.3.0.dist-info/top_level.txt +1 -0
- claude_diary/__init__.py +3 -0
- claude_diary/__main__.py +5 -0
- claude_diary/cli/__init__.py +213 -0
- claude_diary/cli/config.py +190 -0
- claude_diary/cli/maintenance.py +146 -0
- claude_diary/cli/notion_common.py +30 -0
- claude_diary/cli/notion_ensure.py +190 -0
- claude_diary/cli/notion_init.py +148 -0
- claude_diary/cli/notion_ops.py +553 -0
- claude_diary/cli/notion_push/__init__.py +397 -0
- claude_diary/cli/notion_push/artifacts.py +146 -0
- claude_diary/cli/notion_push/ordinals.py +70 -0
- claude_diary/cli/notion_push/preview.py +59 -0
- claude_diary/cli/notion_push/properties.py +234 -0
- claude_diary/cli/notion_push/relations.py +119 -0
- claude_diary/cli/notion_push/tasks.py +75 -0
- claude_diary/cli/notion_push/validate.py +97 -0
- claude_diary/cli/notion_review.py +147 -0
- claude_diary/cli/search.py +158 -0
- claude_diary/cli/setup.py +675 -0
- claude_diary/cli/stats.py +216 -0
- claude_diary/cli/team.py +72 -0
- claude_diary/cli/write.py +309 -0
- claude_diary/config.py +112 -0
- claude_diary/core.py +227 -0
- claude_diary/dashboard.py +308 -0
- claude_diary/exporters/__init__.py +0 -0
- claude_diary/exporters/base.py +30 -0
- claude_diary/exporters/discord.py +73 -0
- claude_diary/exporters/github.py +100 -0
- claude_diary/exporters/loader.py +154 -0
- claude_diary/exporters/notion.py +77 -0
- claude_diary/exporters/notion_hierarchical.py +680 -0
- claude_diary/exporters/notion_views.py +996 -0
- claude_diary/exporters/obsidian.py +80 -0
- claude_diary/exporters/slack.py +60 -0
- claude_diary/formatter.py +710 -0
- claude_diary/hook.py +80 -0
- claude_diary/i18n.py +172 -0
- claude_diary/indexer.py +141 -0
- claude_diary/lib/__init__.py +0 -0
- claude_diary/lib/audit.py +127 -0
- claude_diary/lib/categorizer.py +64 -0
- claude_diary/lib/git_info.py +212 -0
- claude_diary/lib/notion_cache.py +126 -0
- claude_diary/lib/parser.py +216 -0
- claude_diary/lib/secret_scanner.py +85 -0
- claude_diary/lib/stats.py +80 -0
- claude_diary/lib/team_security.py +210 -0
- claude_diary/log.py +42 -0
- claude_diary/team.py +336 -0
- claude_diary/types.py +63 -0
- claude_diary/writer.py +45 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
claude_diary
|
claude_diary/__init__.py
ADDED
claude_diary/__main__.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""claude-diary CLI — search, filter, stats, and manage your work diary."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Re-export dependencies so submodules can access them via claude_diary.cli.*
|
|
8
|
+
# and so that tests can patch them at claude_diary.cli.<name>.
|
|
9
|
+
from claude_diary.config import load_config, save_config, get_config_path, migrate_from_env
|
|
10
|
+
from claude_diary.i18n import get_label
|
|
11
|
+
from claude_diary.indexer import load_index
|
|
12
|
+
from claude_diary.lib.stats import parse_daily_file
|
|
13
|
+
from claude_diary.writer import ensure_diary_dir
|
|
14
|
+
|
|
15
|
+
# Import command functions from submodules
|
|
16
|
+
from claude_diary.cli.search import cmd_search, cmd_filter, cmd_trace, _fallback_search_from_files
|
|
17
|
+
from claude_diary.cli.stats import cmd_stats, cmd_weekly, _get_terminal_width, _print_box_top, _print_box_bottom
|
|
18
|
+
from claude_diary.cli.config import cmd_config, cmd_init, cmd_migrate, _add_exporter_interactive
|
|
19
|
+
from claude_diary.cli.team import cmd_team
|
|
20
|
+
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
|
|
22
|
+
from claude_diary.cli.write import cmd_write
|
|
23
|
+
from claude_diary.cli.notion_push import cmd_notion_push
|
|
24
|
+
from claude_diary.cli.notion_init import cmd_notion_init
|
|
25
|
+
from claude_diary.cli.notion_ensure import cmd_notion_ensure
|
|
26
|
+
from claude_diary.cli.notion_ops import cmd_notion_ops
|
|
27
|
+
from claude_diary.cli.notion_review import cmd_notion_review
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cmd_notion(args):
|
|
31
|
+
"""Dispatch `diary-notion|notion <action>` to the right command."""
|
|
32
|
+
if args.action == "push":
|
|
33
|
+
cmd_notion_push(args)
|
|
34
|
+
elif args.action == "init":
|
|
35
|
+
cmd_notion_init(args)
|
|
36
|
+
elif args.action == "ensure":
|
|
37
|
+
cmd_notion_ensure(args)
|
|
38
|
+
elif args.action == "ops":
|
|
39
|
+
cmd_notion_ops(args)
|
|
40
|
+
elif args.action == "review":
|
|
41
|
+
cmd_notion_review(args)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _add_diary_notion_parser(sub, name):
|
|
45
|
+
p_notion = sub.add_parser(name, help="Notion hierarchical work diary integration")
|
|
46
|
+
p_notion.add_argument("action", choices=["init", "push", "ensure", "ops", "review"],
|
|
47
|
+
help="Action to perform")
|
|
48
|
+
p_notion.add_argument("--input", help="JSON input file (push only)")
|
|
49
|
+
p_notion.add_argument("--force", action="store_true",
|
|
50
|
+
help="Archive prior rows for the session before pushing (push only)")
|
|
51
|
+
p_notion.add_argument("--year", type=int, help="Target year (ensure/ops/review only)")
|
|
52
|
+
p_notion.add_argument("--apply", action="store_true",
|
|
53
|
+
help="Mark the listed rows reviewed (review only)")
|
|
54
|
+
p_notion.add_argument("--dry-run", action="store_true",
|
|
55
|
+
help="Preview push body without writing, or print ensure plan")
|
|
56
|
+
p_notion.add_argument("--preview-file",
|
|
57
|
+
help="Write push dry-run/preview Markdown to this file")
|
|
58
|
+
p_notion.add_argument("--artifact-dir", default=".codefleet/runs",
|
|
59
|
+
help="Directory for local run artifacts (push only)")
|
|
60
|
+
p_notion.add_argument("--no-artifacts", action="store_true",
|
|
61
|
+
help="Do not write local run artifacts (push only)")
|
|
62
|
+
p_notion.add_argument("--stale-days", type=int, default=7,
|
|
63
|
+
help="Mark active rows stale after N days (ops only)")
|
|
64
|
+
p_notion.add_argument("--json", dest="json_output", action="store_true",
|
|
65
|
+
help="Output operations report as JSON (ops only)")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
parser = argparse.ArgumentParser(
|
|
70
|
+
prog="claude-diary",
|
|
71
|
+
description="Auto-generated work diary from Claude Code sessions",
|
|
72
|
+
)
|
|
73
|
+
from claude_diary import __version__
|
|
74
|
+
parser.add_argument("--version", action="version", version="claude-diary %s" % __version__)
|
|
75
|
+
|
|
76
|
+
sub = parser.add_subparsers(dest="command")
|
|
77
|
+
|
|
78
|
+
# search
|
|
79
|
+
p_search = sub.add_parser("search", help="Search diary entries by keyword")
|
|
80
|
+
p_search.add_argument("keyword", help="Keyword to search")
|
|
81
|
+
p_search.add_argument("--project", "-p", help="Filter by project")
|
|
82
|
+
p_search.add_argument("--category", "-c", help="Filter by category")
|
|
83
|
+
p_search.add_argument("--from", dest="date_from", help="Start date (YYYY-MM-DD)")
|
|
84
|
+
p_search.add_argument("--to", dest="date_to", help="End date (YYYY-MM-DD)")
|
|
85
|
+
p_search.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
|
|
86
|
+
|
|
87
|
+
# filter
|
|
88
|
+
p_filter = sub.add_parser("filter", help="Filter diary entries")
|
|
89
|
+
p_filter.add_argument("--project", "-p", help="Filter by project")
|
|
90
|
+
p_filter.add_argument("--category", "-c", help="Filter by category")
|
|
91
|
+
p_filter.add_argument("--month", "-m", help="Filter by month (YYYY-MM)")
|
|
92
|
+
p_filter.add_argument("--json", dest="json_output", action="store_true", help="Output as JSON")
|
|
93
|
+
|
|
94
|
+
# trace
|
|
95
|
+
p_trace = sub.add_parser("trace", help="Trace file change history")
|
|
96
|
+
p_trace.add_argument("filepath", help="File path or glob pattern to trace")
|
|
97
|
+
p_trace.add_argument("--project", "-p", help="Filter by project")
|
|
98
|
+
|
|
99
|
+
# stats
|
|
100
|
+
p_stats = sub.add_parser("stats", help="Show terminal dashboard")
|
|
101
|
+
p_stats.add_argument("--month", "-m", help="Month (YYYY-MM)")
|
|
102
|
+
p_stats.add_argument("--project", "-p", help="Filter by project")
|
|
103
|
+
|
|
104
|
+
# weekly
|
|
105
|
+
p_weekly = sub.add_parser("weekly", help="Generate weekly summary")
|
|
106
|
+
p_weekly.add_argument("date", nargs="?", help="Any date in target week (YYYY-MM-DD)")
|
|
107
|
+
|
|
108
|
+
# config
|
|
109
|
+
p_config = sub.add_parser("config", help="View or update configuration")
|
|
110
|
+
p_config.add_argument("--set", dest="set_value", help="Set config (key=value)")
|
|
111
|
+
p_config.add_argument("--add-exporter", help="Add exporter (interactive)")
|
|
112
|
+
|
|
113
|
+
# init
|
|
114
|
+
p_init = sub.add_parser("init", help="Initialize agent-diary setup")
|
|
115
|
+
init_mode = p_init.add_mutually_exclusive_group()
|
|
116
|
+
init_mode.add_argument("--team", dest="team_repo", help="Team repo URL for team mode")
|
|
117
|
+
init_mode.add_argument("--codex-only", action="store_true",
|
|
118
|
+
help="Initialize config/diary directories without registering Claude Code hooks")
|
|
119
|
+
|
|
120
|
+
# migrate
|
|
121
|
+
sub.add_parser("migrate", help="Migrate v1.0 env vars to config.json")
|
|
122
|
+
|
|
123
|
+
# team
|
|
124
|
+
p_team = sub.add_parser("team", help="Team management commands")
|
|
125
|
+
p_team.add_argument("action", nargs="?", default="stats",
|
|
126
|
+
choices=["stats", "weekly", "monthly", "init", "add-member"],
|
|
127
|
+
help="Team action")
|
|
128
|
+
p_team.add_argument("--project", "-p", help="Filter by project")
|
|
129
|
+
p_team.add_argument("--member", help="Filter by member")
|
|
130
|
+
p_team.add_argument("--month", "-m", help="Month (YYYY-MM)")
|
|
131
|
+
p_team.add_argument("--repo", help="Team repo URL (for init)")
|
|
132
|
+
p_team.add_argument("--name", help="Member name (for init/add-member)")
|
|
133
|
+
p_team.add_argument("--role", default="member", help="Role (for add-member)")
|
|
134
|
+
|
|
135
|
+
# reindex
|
|
136
|
+
sub.add_parser("reindex", help="Rebuild search index")
|
|
137
|
+
|
|
138
|
+
# audit
|
|
139
|
+
p_audit = sub.add_parser("audit", help="View audit log and verify integrity")
|
|
140
|
+
p_audit.add_argument("--days", type=int, help="Show entries from last N days")
|
|
141
|
+
p_audit.add_argument("--verify", action="store_true", help="Verify source code checksum")
|
|
142
|
+
p_audit.add_argument("-n", type=int, default=10, help="Number of entries (default: 10)")
|
|
143
|
+
|
|
144
|
+
# delete
|
|
145
|
+
p_delete = sub.add_parser("delete", help="Delete a diary session entry")
|
|
146
|
+
p_delete.add_argument("--last", action="store_true", help="Delete the last session entry")
|
|
147
|
+
p_delete.add_argument("--session", help="Delete by session ID prefix")
|
|
148
|
+
|
|
149
|
+
# dashboard
|
|
150
|
+
p_dashboard = sub.add_parser("dashboard", help="Generate HTML dashboard")
|
|
151
|
+
p_dashboard.add_argument("--serve", action="store_true", help="Start local server")
|
|
152
|
+
p_dashboard.add_argument("--port", type=int, default=8787, help="Server port (default: 8787)")
|
|
153
|
+
p_dashboard.add_argument("--months", type=int, default=3, help="Months of data (default: 3)")
|
|
154
|
+
|
|
155
|
+
# install / uninstall
|
|
156
|
+
p_install = sub.add_parser("install", help="Register agent-diary hook in Claude Code")
|
|
157
|
+
p_install.add_argument("--force", action="store_true",
|
|
158
|
+
help=("Refresh hook command and overwrite managed slash command files "
|
|
159
|
+
"(preserves user-modified ones)"))
|
|
160
|
+
install_agent = p_install.add_mutually_exclusive_group()
|
|
161
|
+
install_agent.add_argument("--codex", action="store_true",
|
|
162
|
+
help="Also install Codex skills under ~/.codex/skills")
|
|
163
|
+
install_agent.add_argument("--codex-only", action="store_true",
|
|
164
|
+
help="Install only Codex skills; do not modify Claude Code settings")
|
|
165
|
+
p_uninstall = sub.add_parser("uninstall", help="Remove agent-diary hook from Claude Code")
|
|
166
|
+
uninstall_agent = p_uninstall.add_mutually_exclusive_group()
|
|
167
|
+
uninstall_agent.add_argument("--codex", action="store_true",
|
|
168
|
+
help="Also remove Codex skills installed by claude-diary")
|
|
169
|
+
uninstall_agent.add_argument("--codex-only", action="store_true",
|
|
170
|
+
help="Remove only Codex skills; do not modify Claude Code settings")
|
|
171
|
+
|
|
172
|
+
# write (manual diary — for /diary slash command)
|
|
173
|
+
p_write = sub.add_parser("write", help="Write current session diary to <manual_dir>/<date>/<project>/")
|
|
174
|
+
p_write.add_argument("--input", help="JSON input file for agent-authored diary entries")
|
|
175
|
+
|
|
176
|
+
# notion (hierarchical Notion DB integration — for /diary-notion slash command)
|
|
177
|
+
_add_diary_notion_parser(sub, "diary-notion")
|
|
178
|
+
_add_diary_notion_parser(sub, "notion")
|
|
179
|
+
|
|
180
|
+
args = parser.parse_args()
|
|
181
|
+
|
|
182
|
+
if args.command is None:
|
|
183
|
+
parser.print_help()
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
commands = {
|
|
187
|
+
"search": cmd_search,
|
|
188
|
+
"filter": cmd_filter,
|
|
189
|
+
"trace": cmd_trace,
|
|
190
|
+
"stats": cmd_stats,
|
|
191
|
+
"weekly": cmd_weekly,
|
|
192
|
+
"config": cmd_config,
|
|
193
|
+
"init": cmd_init,
|
|
194
|
+
"migrate": cmd_migrate,
|
|
195
|
+
"reindex": cmd_reindex,
|
|
196
|
+
"team": cmd_team,
|
|
197
|
+
"audit": cmd_audit,
|
|
198
|
+
"delete": cmd_delete,
|
|
199
|
+
"dashboard": cmd_dashboard,
|
|
200
|
+
"install": cmd_install,
|
|
201
|
+
"uninstall": cmd_uninstall,
|
|
202
|
+
"write": cmd_write,
|
|
203
|
+
"diary-notion": cmd_notion,
|
|
204
|
+
"notion": cmd_notion,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn = commands.get(args.command)
|
|
208
|
+
if fn:
|
|
209
|
+
fn(args)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
main()
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Config, init, and migrate commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import claude_diary.cli as _cli
|
|
8
|
+
from claude_diary.cli.setup import HOOK_COMMAND
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_config(args):
|
|
12
|
+
config = _cli.load_config()
|
|
13
|
+
|
|
14
|
+
if args.add_exporter:
|
|
15
|
+
_add_exporter_interactive(config, args.add_exporter)
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
if args.set_value:
|
|
19
|
+
key, _, value = args.set_value.partition("=")
|
|
20
|
+
key = key.strip()
|
|
21
|
+
value = value.strip()
|
|
22
|
+
if key == "lang":
|
|
23
|
+
if value not in ("ko", "en"):
|
|
24
|
+
print("Invalid lang: %s (use 'ko' or 'en')" % value)
|
|
25
|
+
return
|
|
26
|
+
config[key] = value
|
|
27
|
+
elif key == "diary_dir":
|
|
28
|
+
config[key] = value
|
|
29
|
+
elif key == "manual_diary_dir":
|
|
30
|
+
config[key] = value
|
|
31
|
+
elif key == "timezone_offset":
|
|
32
|
+
try:
|
|
33
|
+
tz = int(value)
|
|
34
|
+
if not (-12 <= tz <= 14):
|
|
35
|
+
print("Invalid timezone_offset: %s (range: -12 to 14)" % value)
|
|
36
|
+
return
|
|
37
|
+
config[key] = tz
|
|
38
|
+
except ValueError:
|
|
39
|
+
print("Invalid timezone_offset: %s (must be integer)" % value)
|
|
40
|
+
return
|
|
41
|
+
else:
|
|
42
|
+
print("Unknown config key: %s (available: lang, diary_dir, manual_diary_dir, timezone_offset)" % key)
|
|
43
|
+
return
|
|
44
|
+
_cli.save_config(config)
|
|
45
|
+
print("Set %s = %s" % (key, value))
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
# Display current config
|
|
49
|
+
print("Config path: %s" % _cli.get_config_path())
|
|
50
|
+
print()
|
|
51
|
+
for key, value in sorted(config.items()):
|
|
52
|
+
if key == "exporters":
|
|
53
|
+
print("exporters:")
|
|
54
|
+
for name, exp in value.items():
|
|
55
|
+
enabled = exp.get("enabled", False)
|
|
56
|
+
status = "enabled" if enabled else "disabled"
|
|
57
|
+
details = []
|
|
58
|
+
for k, v in exp.items():
|
|
59
|
+
if k == "enabled":
|
|
60
|
+
continue
|
|
61
|
+
if k in ("api_token", "token", "webhook_url") and isinstance(v, str) and len(v) > 8:
|
|
62
|
+
v = v[:4] + "..." + v[-4:]
|
|
63
|
+
details.append("%s=%s" % (k, v))
|
|
64
|
+
detail_str = " (%s)" % ", ".join(details) if details else ""
|
|
65
|
+
print(" %s: %s%s" % (name, status, detail_str))
|
|
66
|
+
elif isinstance(value, dict):
|
|
67
|
+
print("%s: %s" % (key, json.dumps(value, ensure_ascii=False)))
|
|
68
|
+
else:
|
|
69
|
+
print("%s: %s" % (key, value))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _add_exporter_interactive(config, name):
|
|
73
|
+
if "exporters" not in config:
|
|
74
|
+
config["exporters"] = {}
|
|
75
|
+
|
|
76
|
+
if name == "notion":
|
|
77
|
+
token = input("Notion API token: ").strip()
|
|
78
|
+
db_id = input("Notion Database ID: ").strip()
|
|
79
|
+
config["exporters"]["notion"] = {
|
|
80
|
+
"enabled": True,
|
|
81
|
+
"api_token": token,
|
|
82
|
+
"database_id": db_id,
|
|
83
|
+
}
|
|
84
|
+
elif name in ("slack", "discord"):
|
|
85
|
+
url = input("%s Webhook URL: " % name.capitalize()).strip()
|
|
86
|
+
config["exporters"][name] = {"enabled": True, "webhook_url": url}
|
|
87
|
+
elif name == "obsidian":
|
|
88
|
+
path = input("Obsidian vault path: ").strip()
|
|
89
|
+
config["exporters"]["obsidian"] = {"enabled": True, "vault_path": path}
|
|
90
|
+
elif name == "github":
|
|
91
|
+
repo = input("GitHub repo (owner/repo): ").strip()
|
|
92
|
+
mode = input("Mode (repo/wiki/issue) [repo]: ").strip() or "repo"
|
|
93
|
+
config["exporters"]["github"] = {"enabled": True, "repo": repo, "mode": mode}
|
|
94
|
+
else:
|
|
95
|
+
print("Unknown exporter: %s" % name)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
_cli.save_config(config)
|
|
99
|
+
print("Exporter '%s' added and enabled." % name)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cmd_init(args):
|
|
103
|
+
config = _cli.load_config()
|
|
104
|
+
diary_dir = os.path.expanduser(config["diary_dir"])
|
|
105
|
+
|
|
106
|
+
# Team mode init
|
|
107
|
+
if hasattr(args, 'team_repo') and args.team_repo:
|
|
108
|
+
from claude_diary.team import init_team
|
|
109
|
+
print("Initializing claude-diary (team mode)...")
|
|
110
|
+
print()
|
|
111
|
+
init_team(args.team_repo)
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
print("Initializing claude-diary...")
|
|
115
|
+
print()
|
|
116
|
+
|
|
117
|
+
# Create diary directory
|
|
118
|
+
_cli.ensure_diary_dir(diary_dir)
|
|
119
|
+
print(" [ok] Diary directory: %s" % diary_dir)
|
|
120
|
+
|
|
121
|
+
# Save config
|
|
122
|
+
_cli.save_config(config)
|
|
123
|
+
print(" [ok] Config: %s" % _cli.get_config_path())
|
|
124
|
+
|
|
125
|
+
if getattr(args, "codex_only", False) is True:
|
|
126
|
+
print(" [ok] Codex-only init: Claude Code Stop Hook not registered")
|
|
127
|
+
print()
|
|
128
|
+
print("Done! Run `agent-diary install --force --codex-only` to install Codex skills.")
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
# Register Stop Hook
|
|
132
|
+
claude_settings = os.path.join(os.path.expanduser("~"), ".claude", "settings.json")
|
|
133
|
+
if os.path.exists(claude_settings):
|
|
134
|
+
try:
|
|
135
|
+
with open(claude_settings, "r", encoding="utf-8") as f:
|
|
136
|
+
settings = json.load(f)
|
|
137
|
+
except (json.JSONDecodeError, IOError, ValueError):
|
|
138
|
+
settings = {}
|
|
139
|
+
|
|
140
|
+
if "hooks" not in settings:
|
|
141
|
+
settings["hooks"] = {}
|
|
142
|
+
if "Stop" not in settings["hooks"]:
|
|
143
|
+
settings["hooks"]["Stop"] = []
|
|
144
|
+
|
|
145
|
+
# Check if already registered
|
|
146
|
+
already = False
|
|
147
|
+
for group in settings["hooks"]["Stop"]:
|
|
148
|
+
for h in group.get("hooks", []):
|
|
149
|
+
if "hook.py" in h.get("command", "") or "claude_diary" in h.get("command", ""):
|
|
150
|
+
already = True
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
if not already:
|
|
154
|
+
hook_cmd = HOOK_COMMAND
|
|
155
|
+
settings["hooks"]["Stop"].append({
|
|
156
|
+
"hooks": [{"type": "command", "command": hook_cmd}]
|
|
157
|
+
})
|
|
158
|
+
with open(claude_settings, "w", encoding="utf-8") as f:
|
|
159
|
+
json.dump(settings, f, indent=2, ensure_ascii=False)
|
|
160
|
+
print(" [ok] Stop Hook registered: %s" % hook_cmd)
|
|
161
|
+
else:
|
|
162
|
+
print(" [ok] Stop Hook already registered")
|
|
163
|
+
else:
|
|
164
|
+
# Create settings.json with hook registration
|
|
165
|
+
claude_dir = os.path.join(os.path.expanduser("~"), ".claude")
|
|
166
|
+
Path(claude_dir).mkdir(parents=True, exist_ok=True)
|
|
167
|
+
hook_cmd = HOOK_COMMAND
|
|
168
|
+
settings = {
|
|
169
|
+
"hooks": {
|
|
170
|
+
"Stop": [{"hooks": [{"type": "command", "command": hook_cmd}]}]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
with open(claude_settings, "w", encoding="utf-8") as f:
|
|
174
|
+
json.dump(settings, f, indent=2, ensure_ascii=False)
|
|
175
|
+
print(" [ok] Created %s with Stop Hook" % claude_settings)
|
|
176
|
+
|
|
177
|
+
print()
|
|
178
|
+
print("Done! Claude Code sessions will be auto-logged.")
|
|
179
|
+
print(" View diary: cat %s/$(date +%%Y-%%m-%%d).md" % diary_dir)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def cmd_migrate(args):
|
|
183
|
+
print("Migrating v1.0 environment variables to config.json...")
|
|
184
|
+
config = _cli.migrate_from_env()
|
|
185
|
+
print(" lang: %s" % config["lang"])
|
|
186
|
+
print(" diary_dir: %s" % config["diary_dir"])
|
|
187
|
+
print(" timezone_offset: %s" % config["timezone_offset"])
|
|
188
|
+
print()
|
|
189
|
+
print("Config saved: %s" % _cli.get_config_path())
|
|
190
|
+
print("Note: Environment variables still work as fallback.")
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Reindex, audit, delete, and dashboard commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime, timezone, timedelta
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import claude_diary.cli as _cli
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def cmd_reindex(args):
|
|
11
|
+
from claude_diary.indexer import reindex_all
|
|
12
|
+
config = _cli.load_config()
|
|
13
|
+
diary_dir = os.path.expanduser(config["diary_dir"])
|
|
14
|
+
|
|
15
|
+
print("Rebuilding search index...")
|
|
16
|
+
count = reindex_all(diary_dir)
|
|
17
|
+
index_path = os.path.join(diary_dir, ".diary_index.json")
|
|
18
|
+
print("Indexed %d sessions." % count)
|
|
19
|
+
print("Index: %s" % index_path)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cmd_audit(args):
|
|
23
|
+
from claude_diary.lib.audit import read_audit_log, verify_checksum
|
|
24
|
+
config = _cli.load_config()
|
|
25
|
+
diary_dir = os.path.expanduser(config["diary_dir"])
|
|
26
|
+
|
|
27
|
+
if args.verify:
|
|
28
|
+
is_valid, current, last = verify_checksum(diary_dir)
|
|
29
|
+
if is_valid:
|
|
30
|
+
print("Checksum OK: %s" % current)
|
|
31
|
+
else:
|
|
32
|
+
print("WARNING: Checksum mismatch!")
|
|
33
|
+
print(" Current: %s" % current)
|
|
34
|
+
print(" Last log: %s" % last)
|
|
35
|
+
print(" Source files may have been modified since last Hook execution.")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
entries = read_audit_log(diary_dir, days=args.days, limit=args.n)
|
|
39
|
+
|
|
40
|
+
if not entries:
|
|
41
|
+
print("No audit log entries found.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
print("Audit log (%d entries):" % len(entries))
|
|
45
|
+
print()
|
|
46
|
+
for e in entries:
|
|
47
|
+
ts = e.get("timestamp", "")[:19]
|
|
48
|
+
sid = e.get("session_id", "")[:8]
|
|
49
|
+
masked = e.get("secrets_masked", 0)
|
|
50
|
+
written = len(e.get("files_written", []))
|
|
51
|
+
exporters = e.get("exporters_called", [])
|
|
52
|
+
failed = e.get("exporters_failed", [])
|
|
53
|
+
|
|
54
|
+
line = " %s | session:%s | wrote:%d" % (ts, sid, written)
|
|
55
|
+
if masked > 0:
|
|
56
|
+
line += " | secrets_masked:%d" % masked
|
|
57
|
+
if exporters:
|
|
58
|
+
line += " | exporters:%s" % ",".join(exporters)
|
|
59
|
+
if failed:
|
|
60
|
+
line += " | FAILED:%s" % ",".join(failed)
|
|
61
|
+
print(line)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def cmd_delete(args):
|
|
65
|
+
config = _cli.load_config()
|
|
66
|
+
diary_dir = os.path.expanduser(config["diary_dir"])
|
|
67
|
+
tz_offset = config.get("timezone_offset", 9)
|
|
68
|
+
local_tz = timezone(timedelta(hours=tz_offset))
|
|
69
|
+
|
|
70
|
+
if args.last:
|
|
71
|
+
today = datetime.now(local_tz).strftime("%Y-%m-%d")
|
|
72
|
+
filepath = os.path.join(diary_dir, "%s.md" % today)
|
|
73
|
+
if not os.path.exists(filepath):
|
|
74
|
+
print("No diary file for today (%s)" % today)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Confirmation prompt
|
|
78
|
+
confirm = input("Delete last session entry from %s? [y/N]: " % today).strip().lower()
|
|
79
|
+
if confirm not in ("y", "yes"):
|
|
80
|
+
print("Cancelled.")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
84
|
+
content = f.read()
|
|
85
|
+
|
|
86
|
+
# Split by session markers and remove last
|
|
87
|
+
parts = content.split("### ⏰")
|
|
88
|
+
if len(parts) <= 1:
|
|
89
|
+
print("No session entries found in today's diary.")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Remove last entry (everything after last "### ⏰")
|
|
93
|
+
new_content = "### ⏰".join(parts[:-1])
|
|
94
|
+
# Remove trailing "---\n\n" if present
|
|
95
|
+
new_content = new_content.rstrip()
|
|
96
|
+
if new_content.endswith("---"):
|
|
97
|
+
new_content = new_content[:-3].rstrip()
|
|
98
|
+
new_content += "\n\n"
|
|
99
|
+
|
|
100
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
101
|
+
f.write(new_content)
|
|
102
|
+
|
|
103
|
+
print("Last session entry deleted from %s" % filepath)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if args.session:
|
|
107
|
+
# Search all files for session ID and remove that entry
|
|
108
|
+
found = False
|
|
109
|
+
for f in sorted(Path(diary_dir).glob("*.md")):
|
|
110
|
+
try:
|
|
111
|
+
content = f.read_text(encoding="utf-8")
|
|
112
|
+
except Exception:
|
|
113
|
+
continue
|
|
114
|
+
if args.session in content:
|
|
115
|
+
parts = content.split("### ⏰")
|
|
116
|
+
new_parts = [parts[0]]
|
|
117
|
+
for part in parts[1:]:
|
|
118
|
+
if args.session not in part:
|
|
119
|
+
new_parts.append(part)
|
|
120
|
+
else:
|
|
121
|
+
found = True
|
|
122
|
+
new_content = "### ⏰".join(new_parts).rstrip() + "\n"
|
|
123
|
+
f.write_text(new_content, encoding="utf-8")
|
|
124
|
+
print("Session %s deleted from %s" % (args.session, f.name))
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
if not found:
|
|
128
|
+
print("Session '%s' not found." % args.session)
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
print("Specify --last or --session <id>")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_dashboard(args):
|
|
135
|
+
from claude_diary.dashboard import generate_dashboard, serve_dashboard
|
|
136
|
+
config = _cli.load_config()
|
|
137
|
+
diary_dir = os.path.expanduser(config["diary_dir"])
|
|
138
|
+
|
|
139
|
+
path = generate_dashboard(diary_dir, months=args.months)
|
|
140
|
+
print("Dashboard generated: %s" % path)
|
|
141
|
+
|
|
142
|
+
if args.serve:
|
|
143
|
+
serve_dashboard(diary_dir, port=args.port)
|
|
144
|
+
else:
|
|
145
|
+
import webbrowser
|
|
146
|
+
webbrowser.open("file://%s" % os.path.abspath(path))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Helpers shared by the `diary-notion` subcommands that read Notion rows."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone, timedelta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def resolve_year_and_today(config, explicit_year):
|
|
7
|
+
"""Return (year, "YYYY-MM-DD") in the configured local timezone."""
|
|
8
|
+
tz_offset = config.get("timezone_offset", 9)
|
|
9
|
+
local_tz = timezone(timedelta(hours=tz_offset))
|
|
10
|
+
now = datetime.now(local_tz)
|
|
11
|
+
year = explicit_year or now.year
|
|
12
|
+
return year, now.strftime("%Y-%m-%d")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def plain_text(item):
|
|
16
|
+
return item.get("plain_text") or ((item.get("text") or {}).get("content")) or ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def title_value(prop):
|
|
20
|
+
values = (prop or {}).get("title") or []
|
|
21
|
+
text = "".join(plain_text(item) for item in values).strip()
|
|
22
|
+
return text or "(untitled)"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def select_value(prop):
|
|
26
|
+
return ((prop or {}).get("select") or {}).get("name") or ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def date_start_value(prop):
|
|
30
|
+
return ((prop or {}).get("date") or {}).get("start") or ""
|