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.
Files changed (59) hide show
  1. agent_diary-4.3.0.dist-info/METADATA +599 -0
  2. agent_diary-4.3.0.dist-info/RECORD +59 -0
  3. agent_diary-4.3.0.dist-info/WHEEL +5 -0
  4. agent_diary-4.3.0.dist-info/entry_points.txt +4 -0
  5. agent_diary-4.3.0.dist-info/licenses/LICENSE +21 -0
  6. agent_diary-4.3.0.dist-info/top_level.txt +1 -0
  7. claude_diary/__init__.py +3 -0
  8. claude_diary/__main__.py +5 -0
  9. claude_diary/cli/__init__.py +213 -0
  10. claude_diary/cli/config.py +190 -0
  11. claude_diary/cli/maintenance.py +146 -0
  12. claude_diary/cli/notion_common.py +30 -0
  13. claude_diary/cli/notion_ensure.py +190 -0
  14. claude_diary/cli/notion_init.py +148 -0
  15. claude_diary/cli/notion_ops.py +553 -0
  16. claude_diary/cli/notion_push/__init__.py +397 -0
  17. claude_diary/cli/notion_push/artifacts.py +146 -0
  18. claude_diary/cli/notion_push/ordinals.py +70 -0
  19. claude_diary/cli/notion_push/preview.py +59 -0
  20. claude_diary/cli/notion_push/properties.py +234 -0
  21. claude_diary/cli/notion_push/relations.py +119 -0
  22. claude_diary/cli/notion_push/tasks.py +75 -0
  23. claude_diary/cli/notion_push/validate.py +97 -0
  24. claude_diary/cli/notion_review.py +147 -0
  25. claude_diary/cli/search.py +158 -0
  26. claude_diary/cli/setup.py +675 -0
  27. claude_diary/cli/stats.py +216 -0
  28. claude_diary/cli/team.py +72 -0
  29. claude_diary/cli/write.py +309 -0
  30. claude_diary/config.py +112 -0
  31. claude_diary/core.py +227 -0
  32. claude_diary/dashboard.py +308 -0
  33. claude_diary/exporters/__init__.py +0 -0
  34. claude_diary/exporters/base.py +30 -0
  35. claude_diary/exporters/discord.py +73 -0
  36. claude_diary/exporters/github.py +100 -0
  37. claude_diary/exporters/loader.py +154 -0
  38. claude_diary/exporters/notion.py +77 -0
  39. claude_diary/exporters/notion_hierarchical.py +680 -0
  40. claude_diary/exporters/notion_views.py +996 -0
  41. claude_diary/exporters/obsidian.py +80 -0
  42. claude_diary/exporters/slack.py +60 -0
  43. claude_diary/formatter.py +710 -0
  44. claude_diary/hook.py +80 -0
  45. claude_diary/i18n.py +172 -0
  46. claude_diary/indexer.py +141 -0
  47. claude_diary/lib/__init__.py +0 -0
  48. claude_diary/lib/audit.py +127 -0
  49. claude_diary/lib/categorizer.py +64 -0
  50. claude_diary/lib/git_info.py +212 -0
  51. claude_diary/lib/notion_cache.py +126 -0
  52. claude_diary/lib/parser.py +216 -0
  53. claude_diary/lib/secret_scanner.py +85 -0
  54. claude_diary/lib/stats.py +80 -0
  55. claude_diary/lib/team_security.py +210 -0
  56. claude_diary/log.py +42 -0
  57. claude_diary/team.py +336 -0
  58. claude_diary/types.py +63 -0
  59. claude_diary/writer.py +45 -0
@@ -0,0 +1 @@
1
+ claude_diary
@@ -0,0 +1,3 @@
1
+ """Claude Code Working Diary — Auto-generate work diaries from Claude Code sessions."""
2
+
3
+ __version__ = "4.3.0"
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m claude_diary"""
2
+ from claude_diary.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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 ""