worklog-for-claude-code 0.2.0__tar.gz → 0.3.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_code-0.3.0/PKG-INFO +130 -0
- worklog_for_claude_code-0.3.0/README.md +91 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/pyproject.toml +7 -4
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/tests/test_accounts.py +120 -30
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/tests/test_chats.py +77 -7
- worklog_for_claude_code-0.3.0/tests/test_checks.py +103 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/tests/test_demo.py +25 -1
- worklog_for_claude_code-0.3.0/tests/test_platforms.py +284 -0
- worklog_for_claude_code-0.3.0/tests/test_updates.py +112 -0
- worklog_for_claude_code-0.3.0/tests/test_viz.py +78 -0
- worklog_for_claude_code-0.3.0/tests/test_watcher.py +219 -0
- worklog_for_claude_code-0.3.0/tests/test_worklog.py +101 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/__init__.py +1 -1
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/chats.py +63 -6
- worklog_for_claude_code-0.3.0/worklog/checks.py +209 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/cli.py +87 -8
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/collector.py +32 -8
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/config.py +52 -6
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/demo.py +28 -3
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/hooks.py +25 -2
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/identity.py +21 -19
- worklog_for_claude_code-0.3.0/worklog/locks.py +60 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/parser.py +14 -3
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/report.py +65 -12
- worklog_for_claude_code-0.3.0/worklog/services.py +513 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/store.py +18 -20
- worklog_for_claude_code-0.3.0/worklog/updates.py +193 -0
- worklog_for_claude_code-0.3.0/worklog/viz.py +199 -0
- worklog_for_claude_code-0.3.0/worklog/watcher.py +493 -0
- worklog_for_claude_code-0.3.0/worklog/web.py +1537 -0
- worklog_for_claude_code-0.3.0/worklog_for_claude_code.egg-info/PKG-INFO +130 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog_for_claude_code.egg-info/SOURCES.txt +10 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog_for_claude_code.egg-info/requires.txt +1 -1
- worklog_for_claude_code-0.2.0/PKG-INFO +0 -114
- worklog_for_claude_code-0.2.0/README.md +0 -78
- worklog_for_claude_code-0.2.0/tests/test_worklog.py +0 -54
- worklog_for_claude_code-0.2.0/worklog/services.py +0 -139
- worklog_for_claude_code-0.2.0/worklog/web.py +0 -965
- worklog_for_claude_code-0.2.0/worklog_for_claude_code.egg-info/PKG-INFO +0 -114
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/LICENSE +0 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/setup.cfg +0 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog/redact.py +0 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog_for_claude_code.egg-info/dependency_links.txt +0 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog_for_claude_code.egg-info/entry_points.txt +0 -0
- {worklog_for_claude_code-0.2.0 → worklog_for_claude_code-0.3.0}/worklog_for_claude_code.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: worklog-for-claude-code
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: See what you did with Claude Code each day and how many tokens it used: an automatic daily work log across accounts and devices, private by default
|
|
5
|
+
Author: Urvin
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/us-urvin/worklog-for-claude-code
|
|
8
|
+
Project-URL: Source, https://github.com/us-urvin/worklog-for-claude-code
|
|
9
|
+
Project-URL: Issues, https://github.com/us-urvin/worklog-for-claude-code/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GETTING-STARTED.md
|
|
11
|
+
Project-URL: Changelog, https://github.com/us-urvin/worklog-for-claude-code/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: claude-code,claude,claude-ai,anthropic,worklog,time-tracking,token-usage,token-tracker,usage-tracker,standup,developer-tools,daily-report,productivity
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Software Development
|
|
27
|
+
Classifier: Topic :: Utilities
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff==0.17.0; extra == "dev"
|
|
37
|
+
Requires-Dist: build; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# Worklog for Claude Code
|
|
41
|
+
|
|
42
|
+
**A diary of your Claude Code work, written for you. See what you did each day and how many tokens it used.**
|
|
43
|
+
|
|
44
|
+
[](https://pepy.tech/project/worklog-for-claude-code)
|
|
45
|
+
[](https://pypi.org/project/worklog-for-claude-code/)
|
|
46
|
+
[](https://github.com/us-urvin/worklog-for-claude-code/stargazers)
|
|
47
|
+
[](https://github.com/us-urvin/worklog-for-claude-code/actions/workflows/ci.yml)
|
|
48
|
+
[](https://github.com/us-urvin/worklog-for-claude-code/blob/main/LICENSE)
|
|
49
|
+
|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
▶️ **[Watch the 90-second video](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/media/worklog-demo.mp4)**
|
|
53
|
+
|
|
54
|
+
## What it does
|
|
55
|
+
|
|
56
|
+
- ✅ **Writes down what you worked on**, project by project, every day. Claude writes the summary.
|
|
57
|
+
- 📊 **Shows how many tokens you used**, per day, account and model.
|
|
58
|
+
- 👥 **Handles several Claude accounts and computers**, even if you switch accounts with a one-click app.
|
|
59
|
+
- 🔒 **Stays private**: everything lives on your computer (and your own private git repo, if you want).
|
|
60
|
+
- 💻 **Runs on Linux, macOS and Windows**, quietly in the background.
|
|
61
|
+
|
|
62
|
+
## Try it (nothing is installed)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pipx run --spec worklog-for-claude-code claude-worklog demo
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Open **http://localhost:8766** to see it with made-up data. Press **Ctrl+C** to stop.
|
|
69
|
+
|
|
70
|
+
## Install in 3 steps
|
|
71
|
+
|
|
72
|
+
You need **Python 3.10+**, **git** and **[Claude Code](https://code.claude.com/docs)**. No pipx yet? See the [beginner guide](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GETTING-STARTED.md).
|
|
73
|
+
|
|
74
|
+
**1. Install**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pipx install worklog-for-claude-code
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**2. Create a folder for your log and start Worklog**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git init ~/claude-worklog-data # Windows PowerShell: git init "$HOME\claude-worklog-data"
|
|
84
|
+
claude-worklog install-service
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**3. Open http://worklog.localhost:8765** (or http://localhost:8765) and fill in the short form.
|
|
88
|
+
|
|
89
|
+
A checklist then shows anything still missing, with the exact command for your computer. When everything is ticked, it disappears. That's it: use Claude Code as usual.
|
|
90
|
+
|
|
91
|
+
## A closer look
|
|
92
|
+
|
|
93
|
+
<table><tr>
|
|
94
|
+
<td width="50%"><img src="https://raw.githubusercontent.com/us-urvin/worklog-for-claude-code/main/docs/images/insights.png" alt="When you worked, the last 5 weeks and today's projects"><br>When you worked, your streak, and where the time went.</td>
|
|
95
|
+
<td width="50%"><img src="https://raw.githubusercontent.com/us-urvin/worklog-for-claude-code/main/docs/images/checklist.png" alt="Setup checklist"><br>A setup checklist with steps for your system.</td>
|
|
96
|
+
</tr><tr>
|
|
97
|
+
<td width="50%"><img src="https://raw.githubusercontent.com/us-urvin/worklog-for-claude-code/main/docs/images/notice.png" alt="New account found"><br>New accounts are noticed for you.</td>
|
|
98
|
+
<td width="50%"><img src="https://raw.githubusercontent.com/us-urvin/worklog-for-claude-code/main/docs/images/update.png" alt="A new version is out: Update now"><br>Updates in one click.</td>
|
|
99
|
+
</tr></table>
|
|
100
|
+
|
|
101
|
+
## Privacy
|
|
102
|
+
|
|
103
|
+
- Only short notes are saved: the first 300 characters of your prompts, token counts, file names and commit messages. Never your files or Claude's replies.
|
|
104
|
+
- Common secrets (API keys, passwords, e-mails) are blanked out before saving. This is best effort, so keep your data repo **private**.
|
|
105
|
+
- Summaries are written by your own Claude account. Turn them off in Settings to stay fully offline.
|
|
106
|
+
- The only other request: once a day, Worklog asks pypi.org whether a new version is out. Nothing about you is sent, and you can turn it off.
|
|
107
|
+
|
|
108
|
+
[Privacy in detail →](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GUIDE.md#privacy)
|
|
109
|
+
|
|
110
|
+
## Questions
|
|
111
|
+
|
|
112
|
+
**It says Sonnet, but I use Opus?** The "By model" list shows what Claude Code really used. To get better summaries, pick **Opus** in Settings → Summaries.
|
|
113
|
+
|
|
114
|
+
**Several accounts or computers?** Settings walks you through it. For several computers, see [Full setup](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GUIDE.md#full-setup).
|
|
115
|
+
|
|
116
|
+
**How do I update?** Click **Update now** when the dashboard says a new version is out, or run `pipx upgrade worklog-for-claude-code`.
|
|
117
|
+
|
|
118
|
+
**Something isn't working?** Run `claude-worklog doctor`, or see [Troubleshooting](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/TROUBLESHOOTING.md).
|
|
119
|
+
|
|
120
|
+
**How do I remove it?** `claude-worklog uninstall-service`, then `pipx uninstall worklog-for-claude-code`. Your log stays.
|
|
121
|
+
|
|
122
|
+
## Like it? ⭐
|
|
123
|
+
|
|
124
|
+
If Worklog saves you time, please **[star it on GitHub](https://github.com/us-urvin/worklog-for-claude-code)**. It helps others find it.
|
|
125
|
+
|
|
126
|
+
📘 [Beginner guide](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GETTING-STARTED.md) · 📖 [Full guide](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GUIDE.md) · 🐛 [Report a bug](https://github.com/us-urvin/worklog-for-claude-code/issues/new/choose) · 📝 [Changelog](https://github.com/us-urvin/worklog-for-claude-code/blob/main/CHANGELOG.md)
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
[MIT](https://github.com/us-urvin/worklog-for-claude-code/blob/main/LICENSE) © 2026 Urvin · Not affiliated with or endorsed by Anthropic.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Worklog for Claude Code
|
|
2
|
+
|
|
3
|
+
**A diary of your Claude Code work, written for you. See what you did each day and how many tokens it used.**
|
|
4
|
+
|
|
5
|
+
[](https://pepy.tech/project/worklog-for-claude-code)
|
|
6
|
+
[](https://pypi.org/project/worklog-for-claude-code/)
|
|
7
|
+
[](https://github.com/us-urvin/worklog-for-claude-code/stargazers)
|
|
8
|
+
[](https://github.com/us-urvin/worklog-for-claude-code/actions/workflows/ci.yml)
|
|
9
|
+
[](https://github.com/us-urvin/worklog-for-claude-code/blob/main/LICENSE)
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
▶️ **[Watch the 90-second video](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/media/worklog-demo.mp4)**
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
- ✅ **Writes down what you worked on**, project by project, every day. Claude writes the summary.
|
|
18
|
+
- 📊 **Shows how many tokens you used**, per day, account and model.
|
|
19
|
+
- 👥 **Handles several Claude accounts and computers**, even if you switch accounts with a one-click app.
|
|
20
|
+
- 🔒 **Stays private**: everything lives on your computer (and your own private git repo, if you want).
|
|
21
|
+
- 💻 **Runs on Linux, macOS and Windows**, quietly in the background.
|
|
22
|
+
|
|
23
|
+
## Try it (nothing is installed)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pipx run --spec worklog-for-claude-code claude-worklog demo
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Open **http://localhost:8766** to see it with made-up data. Press **Ctrl+C** to stop.
|
|
30
|
+
|
|
31
|
+
## Install in 3 steps
|
|
32
|
+
|
|
33
|
+
You need **Python 3.10+**, **git** and **[Claude Code](https://code.claude.com/docs)**. No pipx yet? See the [beginner guide](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GETTING-STARTED.md).
|
|
34
|
+
|
|
35
|
+
**1. Install**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pipx install worklog-for-claude-code
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**2. Create a folder for your log and start Worklog**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git init ~/claude-worklog-data # Windows PowerShell: git init "$HOME\claude-worklog-data"
|
|
45
|
+
claude-worklog install-service
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**3. Open http://worklog.localhost:8765** (or http://localhost:8765) and fill in the short form.
|
|
49
|
+
|
|
50
|
+
A checklist then shows anything still missing, with the exact command for your computer. When everything is ticked, it disappears. That's it: use Claude Code as usual.
|
|
51
|
+
|
|
52
|
+
## A closer look
|
|
53
|
+
|
|
54
|
+
<table><tr>
|
|
55
|
+
<td width="50%"><img src="https://raw.githubusercontent.com/us-urvin/worklog-for-claude-code/main/docs/images/insights.png" alt="When you worked, the last 5 weeks and today's projects"><br>When you worked, your streak, and where the time went.</td>
|
|
56
|
+
<td width="50%"><img src="https://raw.githubusercontent.com/us-urvin/worklog-for-claude-code/main/docs/images/checklist.png" alt="Setup checklist"><br>A setup checklist with steps for your system.</td>
|
|
57
|
+
</tr><tr>
|
|
58
|
+
<td width="50%"><img src="https://raw.githubusercontent.com/us-urvin/worklog-for-claude-code/main/docs/images/notice.png" alt="New account found"><br>New accounts are noticed for you.</td>
|
|
59
|
+
<td width="50%"><img src="https://raw.githubusercontent.com/us-urvin/worklog-for-claude-code/main/docs/images/update.png" alt="A new version is out: Update now"><br>Updates in one click.</td>
|
|
60
|
+
</tr></table>
|
|
61
|
+
|
|
62
|
+
## Privacy
|
|
63
|
+
|
|
64
|
+
- Only short notes are saved: the first 300 characters of your prompts, token counts, file names and commit messages. Never your files or Claude's replies.
|
|
65
|
+
- Common secrets (API keys, passwords, e-mails) are blanked out before saving. This is best effort, so keep your data repo **private**.
|
|
66
|
+
- Summaries are written by your own Claude account. Turn them off in Settings to stay fully offline.
|
|
67
|
+
- The only other request: once a day, Worklog asks pypi.org whether a new version is out. Nothing about you is sent, and you can turn it off.
|
|
68
|
+
|
|
69
|
+
[Privacy in detail →](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GUIDE.md#privacy)
|
|
70
|
+
|
|
71
|
+
## Questions
|
|
72
|
+
|
|
73
|
+
**It says Sonnet, but I use Opus?** The "By model" list shows what Claude Code really used. To get better summaries, pick **Opus** in Settings → Summaries.
|
|
74
|
+
|
|
75
|
+
**Several accounts or computers?** Settings walks you through it. For several computers, see [Full setup](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GUIDE.md#full-setup).
|
|
76
|
+
|
|
77
|
+
**How do I update?** Click **Update now** when the dashboard says a new version is out, or run `pipx upgrade worklog-for-claude-code`.
|
|
78
|
+
|
|
79
|
+
**Something isn't working?** Run `claude-worklog doctor`, or see [Troubleshooting](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/TROUBLESHOOTING.md).
|
|
80
|
+
|
|
81
|
+
**How do I remove it?** `claude-worklog uninstall-service`, then `pipx uninstall worklog-for-claude-code`. Your log stays.
|
|
82
|
+
|
|
83
|
+
## Like it? ⭐
|
|
84
|
+
|
|
85
|
+
If Worklog saves you time, please **[star it on GitHub](https://github.com/us-urvin/worklog-for-claude-code)**. It helps others find it.
|
|
86
|
+
|
|
87
|
+
📘 [Beginner guide](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GETTING-STARTED.md) · 📖 [Full guide](https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GUIDE.md) · 🐛 [Report a bug](https://github.com/us-urvin/worklog-for-claude-code/issues/new/choose) · 📝 [Changelog](https://github.com/us-urvin/worklog-for-claude-code/blob/main/CHANGELOG.md)
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
[MIT](https://github.com/us-urvin/worklog-for-claude-code/blob/main/LICENSE) © 2026 Urvin · Not affiliated with or endorsed by Anthropic.
|
|
@@ -5,19 +5,21 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
# Distribution name on PyPI. The command stays `claude-worklog` and the package stays `worklog`.
|
|
7
7
|
name = "worklog-for-claude-code"
|
|
8
|
-
version = "0.
|
|
9
|
-
description = "
|
|
8
|
+
version = "0.3.0"
|
|
9
|
+
description = "See what you did with Claude Code each day and how many tokens it used: an automatic daily work log across accounts and devices, private by default"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
license = "MIT"
|
|
13
13
|
license-files = ["LICENSE"]
|
|
14
14
|
authors = [{ name = "Urvin" }]
|
|
15
|
-
keywords = ["claude-code", "claude", "claude-ai", "worklog", "time-tracking", "token-usage", "developer-tools", "daily-report"]
|
|
15
|
+
keywords = ["claude-code", "claude", "claude-ai", "anthropic", "worklog", "time-tracking", "token-usage", "token-tracker", "usage-tracker", "standup", "developer-tools", "daily-report", "productivity"]
|
|
16
16
|
classifiers = [
|
|
17
17
|
"Development Status :: 4 - Beta",
|
|
18
18
|
"Environment :: Console",
|
|
19
19
|
"Environment :: Web Environment",
|
|
20
20
|
"Intended Audience :: Developers",
|
|
21
|
+
"Operating System :: MacOS",
|
|
22
|
+
"Operating System :: Microsoft :: Windows",
|
|
21
23
|
"Operating System :: POSIX :: Linux",
|
|
22
24
|
"Programming Language :: Python :: 3",
|
|
23
25
|
"Programming Language :: Python :: 3 :: Only",
|
|
@@ -33,12 +35,13 @@ dependencies = ['tomli>=2.0; python_version < "3.11"']
|
|
|
33
35
|
|
|
34
36
|
[project.optional-dependencies]
|
|
35
37
|
test = ["pytest>=7"]
|
|
36
|
-
dev = ["pytest>=7", "ruff==0.
|
|
38
|
+
dev = ["pytest>=7", "ruff==0.17.0", "build"]
|
|
37
39
|
|
|
38
40
|
[project.urls]
|
|
39
41
|
Homepage = "https://github.com/us-urvin/worklog-for-claude-code"
|
|
40
42
|
Source = "https://github.com/us-urvin/worklog-for-claude-code"
|
|
41
43
|
Issues = "https://github.com/us-urvin/worklog-for-claude-code/issues"
|
|
44
|
+
Documentation = "https://github.com/us-urvin/worklog-for-claude-code/blob/main/docs/GETTING-STARTED.md"
|
|
42
45
|
Changelog = "https://github.com/us-urvin/worklog-for-claude-code/blob/main/CHANGELOG.md"
|
|
43
46
|
|
|
44
47
|
[project.scripts]
|
|
@@ -13,7 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
|
|
14
14
|
import pytest
|
|
15
15
|
|
|
16
|
-
from worklog import cli, collector, identity,
|
|
16
|
+
from worklog import cli, collector, identity, web
|
|
17
17
|
from worklog.config import Account, Config, ConfigError, dump_config, load_config
|
|
18
18
|
from worklog.hooks import install_hook, installed_events
|
|
19
19
|
|
|
@@ -21,23 +21,7 @@ UID_A, EMAIL_A = "11111111-aaaa-4aaa-8aaa-111111111111", "alice.personal@example
|
|
|
21
21
|
UID_B, EMAIL_B = "22222222-bbbb-4bbb-8bbb-222222222222", "bob.work@example.org"
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
# ------------------------------------------------------------------ fixtures
|
|
25
|
-
@pytest.fixture
|
|
26
|
-
def home(tmp_path, monkeypatch):
|
|
27
|
-
"""A throwaway HOME; state and logs are redirected into it too."""
|
|
28
|
-
h = tmp_path / "home"
|
|
29
|
-
(h / ".claude" / "projects").mkdir(parents=True)
|
|
30
|
-
for var, sub in (("HOME", ""), ("XDG_CONFIG_HOME", ".config"), ("XDG_STATE_HOME", ".local/state"),
|
|
31
|
-
("XDG_DATA_HOME", ".local/share")):
|
|
32
|
-
monkeypatch.setenv(var, str(h / sub) if sub else str(h))
|
|
33
|
-
monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
|
|
34
|
-
state = h / ".local/state/claude-worklog"
|
|
35
|
-
for mod in (identity, store, cli):
|
|
36
|
-
monkeypatch.setattr(mod, "STATE_DIR", state)
|
|
37
|
-
assert Path.home() == h # guard: never run against the real home
|
|
38
|
-
return h
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
# ------------------------------------------------------------------ helpers (fixtures: conftest.py)
|
|
41
25
|
def login(home: Path, uid: str | None, email: str | None, path: Path | None = None) -> None:
|
|
42
26
|
"""Simulate `/login`: write oauthAccount (no credentials) into the fake ~/.claude.json."""
|
|
43
27
|
path = path or home / ".claude.json"
|
|
@@ -61,25 +45,18 @@ def transcript(home: Path, sid: str, ts: datetime, cwd: Path, tokens: int = 100)
|
|
|
61
45
|
(d / f"{sid}.jsonl").write_text("\n".join(json.dumps(x) for x in lines) + "\n")
|
|
62
46
|
|
|
63
47
|
|
|
64
|
-
@pytest.fixture
|
|
65
|
-
def repo(home):
|
|
66
|
-
r = home / "data"
|
|
67
|
-
r.mkdir()
|
|
68
|
-
subprocess.run(["git", "init", "-q", str(r)], check=True)
|
|
69
|
-
return r
|
|
70
|
-
|
|
71
|
-
|
|
72
48
|
def make_cfg(home: Path, repo: Path, extra: str = "") -> Config:
|
|
73
49
|
p = home / ".config/claude-worklog/config.toml"
|
|
74
50
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
-
p.write_text(f'device_name = "dev1"\nstore_repo =
|
|
51
|
+
p.write_text(f'device_name = "dev1"\nstore_repo = {json.dumps(str(repo))}\n'
|
|
76
52
|
f'[[accounts]]\nname = "personal"\nconfig_dir = "~/.claude"\n{extra}\n'
|
|
77
53
|
'[report]\nuse_ai = false\n')
|
|
78
54
|
return load_config(p)
|
|
79
55
|
|
|
80
56
|
|
|
81
57
|
def records(repo: Path) -> dict[str, dict]:
|
|
82
|
-
return {
|
|
58
|
+
return {p.relative_to(repo).as_posix(): json.loads(p.read_text(encoding="utf-8"))
|
|
59
|
+
for p in sorted(repo.rglob("records/**/*.json"))}
|
|
83
60
|
|
|
84
61
|
|
|
85
62
|
def start_session(home: Path, sid: str, capsys=None) -> None:
|
|
@@ -132,7 +109,8 @@ def test_session_start_hook_records_account_privately(home, capsys):
|
|
|
132
109
|
start_session(home, "sess-1")
|
|
133
110
|
assert capsys.readouterr().out == "" # SessionStart stdout would be injected into Claude's context
|
|
134
111
|
state_file = home / ".local/state/claude-worklog/accounts.json"
|
|
135
|
-
|
|
112
|
+
if os.name != "nt": # Windows has no Unix modes; files there are private to the user profile
|
|
113
|
+
assert (state_file.stat().st_mode & 0o777) == 0o600
|
|
136
114
|
assert identity.session_logins()["sess-1"] == identity.Identity(UID_A, EMAIL_A)
|
|
137
115
|
log_text = (home / ".local/state/claude-worklog/worklog.log").read_text()
|
|
138
116
|
assert EMAIL_A not in log_text and UID_A not in log_text
|
|
@@ -281,7 +259,7 @@ def test_detected_accounts_panel_masks_and_names(home, repo, monkeypatch):
|
|
|
281
259
|
identity.record_session("s", home / ".claude", identity.Identity(UID_B, "<b>" + EMAIL_B), "start")
|
|
282
260
|
html_out = web._detected_section(cfg, '<input type="hidden" name="csrf" value="t">')
|
|
283
261
|
assert EMAIL_B not in html_out and UID_B not in html_out and "<b><" not in html_out
|
|
284
|
-
assert "<***@example.org" in html_out and "
|
|
262
|
+
assert "<***@example.org" in html_out and "Counted as personal" in html_out # only login in its folder
|
|
285
263
|
|
|
286
264
|
saved = []
|
|
287
265
|
monkeypatch.setattr(web, "save_config", lambda c: saved.append(c))
|
|
@@ -297,3 +275,115 @@ def test_detected_accounts_panel_masks_and_names(home, repo, monkeypatch):
|
|
|
297
275
|
{"ref": ref, "dir": "~/.claude", "name": "bad name!"}):
|
|
298
276
|
n = len(saved)
|
|
299
277
|
assert app._account_name(cfg, form)[1] and len(saved) == n
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ------------------------------------------------------------------ 0.3.0: summary model, re-filing history
|
|
281
|
+
def _wait_for_job(app, timeout=10.0):
|
|
282
|
+
import time
|
|
283
|
+
deadline = time.monotonic() + timeout
|
|
284
|
+
while app.jobs.snapshot().get("running"):
|
|
285
|
+
assert time.monotonic() < deadline, "background job did not finish"
|
|
286
|
+
time.sleep(0.05)
|
|
287
|
+
return app.jobs.snapshot()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_naming_a_login_refiles_older_days_too(home, repo, monkeypatch):
|
|
291
|
+
"""Naming a login must fix every day Claude Code still has, not just the last two."""
|
|
292
|
+
cfg = make_cfg(home, repo)
|
|
293
|
+
old = (datetime.now().astimezone() - timedelta(days=5)).replace(hour=12, minute=0, second=0, microsecond=0)
|
|
294
|
+
d = old.date().isoformat()
|
|
295
|
+
login(home, UID_B, EMAIL_B)
|
|
296
|
+
start_session(home, "s-old")
|
|
297
|
+
transcript(home, "s-old", old, home)
|
|
298
|
+
collector.collect(cfg, [old.date()])
|
|
299
|
+
assert f"records/{d}/dev1/personal/s-old.json" in records(repo) # unnamed login -> folder default
|
|
300
|
+
|
|
301
|
+
from worklog.config import save_config
|
|
302
|
+
monkeypatch.setattr(web, "save_config", lambda c: save_config(c, c.path))
|
|
303
|
+
monkeypatch.setattr(web, "_try_config", lambda: load_config(cfg.path))
|
|
304
|
+
app = web.App(8765)
|
|
305
|
+
loc, err, _ = app.post("/accounts/name", {"ref": identity.ref(UID_B), "dir": "~/.claude", "name": "work"})
|
|
306
|
+
assert err is None and loc.startswith("/settings")
|
|
307
|
+
state = _wait_for_job(app)
|
|
308
|
+
assert state["ok"], state["message"]
|
|
309
|
+
recs = records(repo)
|
|
310
|
+
assert f"records/{d}/dev1/work/s-old.json" in recs
|
|
311
|
+
assert f"records/{d}/dev1/personal/s-old.json" not in recs
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_summary_uses_the_configured_model(home, repo, monkeypatch):
|
|
315
|
+
from worklog import report
|
|
316
|
+
cfg = make_cfg(home, repo)
|
|
317
|
+
calls = []
|
|
318
|
+
|
|
319
|
+
def fake_run(cmd, **kw):
|
|
320
|
+
calls.append(cmd)
|
|
321
|
+
return subprocess.CompletedProcess(cmd, 0, "- did a thing\n", "")
|
|
322
|
+
monkeypatch.setattr(report.shutil, "which", lambda _: "/usr/bin/claude")
|
|
323
|
+
monkeypatch.setattr(report.subprocess, "run", fake_run)
|
|
324
|
+
p = report.ProjectDay("k", "proj", "git", None)
|
|
325
|
+
p.prompts.append(("09:00", "fix the bug"))
|
|
326
|
+
|
|
327
|
+
assert report.ai_summary(p, datetime.now().date(), cfg) == ["- did a thing"]
|
|
328
|
+
assert "--model" not in calls[-1] # default: the account's own default model
|
|
329
|
+
|
|
330
|
+
cfg.summary_model = "opus"
|
|
331
|
+
report.ai_summary(p, datetime.now().date(), cfg)
|
|
332
|
+
i = calls[-1].index("--model")
|
|
333
|
+
assert calls[-1][i + 1] == "opus"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_summary_model_config_roundtrip_and_validation(home, repo):
|
|
337
|
+
cfg = make_cfg(home, repo)
|
|
338
|
+
assert cfg.summary_model == ""
|
|
339
|
+
cfg.summary_model = "claude-opus-5-5"
|
|
340
|
+
cfg.path.write_text(dump_config(cfg))
|
|
341
|
+
assert load_config(cfg.path).summary_model == "claude-opus-5-5"
|
|
342
|
+
for bad in ("--dangerously-skip-permissions", "opus 5", "a" * 101, "op;us"):
|
|
343
|
+
cfg.path.write_text(dump_config(cfg).replace('"claude-opus-5-5"', json.dumps(bad)))
|
|
344
|
+
with pytest.raises(ConfigError):
|
|
345
|
+
load_config(cfg.path)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def test_settings_form_saves_summary_model(home, repo, monkeypatch):
|
|
349
|
+
cfg = make_cfg(home, repo)
|
|
350
|
+
saved = []
|
|
351
|
+
monkeypatch.setattr(web, "save_config", lambda c: saved.append(c))
|
|
352
|
+
monkeypatch.setattr(web, "_try_config", lambda: load_config(cfg.path))
|
|
353
|
+
app = web.App(8765)
|
|
354
|
+
base = {"summarizer": "personal", "claude_bin": "claude", "use_ai": "1"}
|
|
355
|
+
assert app.post("/settings/report", {**base, "model": "opus"})[1] is None
|
|
356
|
+
assert saved[-1].summary_model == "opus"
|
|
357
|
+
assert app.post("/settings/report", {**base, "model": "custom", "model_custom": "claude-opus-5-5"})[1] is None
|
|
358
|
+
assert saved[-1].summary_model == "claude-opus-5-5"
|
|
359
|
+
assert app.post("/settings/report", {**base, "model": ""})[1] is None
|
|
360
|
+
assert saved[-1].summary_model == ""
|
|
361
|
+
n = len(saved)
|
|
362
|
+
assert app.post("/settings/report", {**base, "model": "custom", "model_custom": "--evil"})[1]
|
|
363
|
+
assert len(saved) == n
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def test_doctor_shows_default_and_summary_models(home, repo, capsys, monkeypatch):
|
|
367
|
+
cfg = make_cfg(home, repo)
|
|
368
|
+
cfg.use_ai, cfg.summary_model = True, "opus"
|
|
369
|
+
cfg.path.write_text(dump_config(cfg))
|
|
370
|
+
monkeypatch.setattr(cli, "load_config", lambda: load_config(cfg.path))
|
|
371
|
+
(home / ".claude" / "settings.json").write_text(json.dumps({"model": "claude-sonnet-5-5"}))
|
|
372
|
+
cli.main(["doctor"])
|
|
373
|
+
out = capsys.readouterr().out
|
|
374
|
+
assert "default model: Sonnet 5.5 (claude-sonnet-5-5)" in out
|
|
375
|
+
assert "summaries are written with: opus" in out
|
|
376
|
+
(home / ".claude" / "settings.json").write_text("{}")
|
|
377
|
+
cli.main(["doctor"])
|
|
378
|
+
assert "default model: not set: the plan default" in capsys.readouterr().out
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_login_panel_only_asks_for_a_name_when_logins_share_a_folder(home, repo):
|
|
382
|
+
cfg = make_cfg(home, repo)
|
|
383
|
+
c = '<input type="hidden" name="csrf" value="t">'
|
|
384
|
+
identity.record_session("s1", home / ".claude", identity.Identity(UID_A, EMAIL_A), "start")
|
|
385
|
+
one = web._detected_section(cfg, c)
|
|
386
|
+
assert "Counted as personal" in one and "/accounts/name" not in one # its own folder: nothing to name
|
|
387
|
+
identity.record_session("s2", home / ".claude", identity.Identity(UID_B, EMAIL_B), "start")
|
|
388
|
+
two = web._detected_section(cfg, c)
|
|
389
|
+
assert two.count('action="/accounts/name"') == 2 and "No name yet" in two # /login switching: name them
|
|
@@ -68,8 +68,9 @@ D = date(2026, 9, 20)
|
|
|
68
68
|
def home(tmp_path, monkeypatch):
|
|
69
69
|
h = tmp_path / "home"
|
|
70
70
|
(h / ".claude" / "projects").mkdir(parents=True)
|
|
71
|
-
for var, sub in (("HOME", ""), ("
|
|
72
|
-
("XDG_DATA_HOME", ".local/share")
|
|
71
|
+
for var, sub in (("HOME", ""), ("USERPROFILE", ""), ("XDG_CONFIG_HOME", ".config"),
|
|
72
|
+
("XDG_STATE_HOME", ".local/state"), ("XDG_DATA_HOME", ".local/share"),
|
|
73
|
+
("APPDATA", "AppData/Roaming"), ("LOCALAPPDATA", "AppData/Local")):
|
|
73
74
|
monkeypatch.setenv(var, str(h / sub) if sub else str(h))
|
|
74
75
|
state = h / ".local/state/claude-worklog"
|
|
75
76
|
for mod in (identity, store, cli, chats):
|
|
@@ -85,7 +86,7 @@ def cfg(home):
|
|
|
85
86
|
subprocess.run(["git", "init", "-q", str(repo)], check=True)
|
|
86
87
|
p = home / ".config/claude-worklog/config.toml"
|
|
87
88
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
-
p.write_text(f'device_name = "dev1"\nstore_repo =
|
|
89
|
+
p.write_text(f'device_name = "dev1"\nstore_repo = {json.dumps(str(repo))}\n'
|
|
89
90
|
'[[accounts]]\nname = "personal"\nconfig_dir = "~/.claude"\n'
|
|
90
91
|
'[[accounts]]\nname = "work"\nconfig_dir = "~/.claude-work"\n'
|
|
91
92
|
'[collect]\nmax_prompt_chars = 40\n[report]\nuse_ai = false\n')
|
|
@@ -93,7 +94,7 @@ def cfg(home):
|
|
|
93
94
|
|
|
94
95
|
|
|
95
96
|
def records(cfg) -> dict[str, dict]:
|
|
96
|
-
return {
|
|
97
|
+
return {p.relative_to(cfg.store_repo).as_posix(): json.loads(p.read_text(encoding="utf-8"))
|
|
97
98
|
for p in sorted(cfg.store_repo.rglob("records/**/*.json"))}
|
|
98
99
|
|
|
99
100
|
|
|
@@ -201,7 +202,11 @@ def test_record_redacts_truncates_and_never_holds_claudes_text(cfg):
|
|
|
201
202
|
|
|
202
203
|
|
|
203
204
|
def test_untitled_chat_and_assistant_only_day():
|
|
204
|
-
|
|
205
|
+
long = "please help me plan the database migration for the orders service " * 3
|
|
206
|
+
days, _ = chats.split_days(conv("c1", [human(long + "\nsecond line", ts(D, 9))], name=""))
|
|
207
|
+
title = chats.build_record(days[D], "personal", load_cfg_stub())["title"]
|
|
208
|
+
assert title.startswith("please help me plan") and title.endswith("…") and len(title) <= 80
|
|
209
|
+
days, _ = chats.split_days(conv("c3", [assistant(ts(D, 9), [tool("Bash", "toolu_01X")])], name=""))
|
|
205
210
|
assert chats.build_record(days[D], "personal", load_cfg_stub())["title"] == "Untitled chat"
|
|
206
211
|
days, _ = chats.split_days(conv("c2", [assistant(ts(D, 9))]))
|
|
207
212
|
assert chats.build_record(days[D], "personal", load_cfg_stub()) is None
|
|
@@ -251,7 +256,9 @@ def test_account_choice_is_remembered_and_records_move(cfg, tmp_path):
|
|
|
251
256
|
assert res.account == "work" and list(records(cfg)) == [f"records/{D.isoformat()}/dev1/work/chat-c1.json"]
|
|
252
257
|
assert chats.import_chats(cfg, z).account == "work" # remembered for this claude.ai login
|
|
253
258
|
state = json.loads(chats.state_path().read_text())
|
|
254
|
-
assert ACCT not in json.dumps(state)
|
|
259
|
+
assert ACCT not in json.dumps(state)
|
|
260
|
+
if os.name != "nt": # Windows has no Unix modes; files there are private to the user profile
|
|
261
|
+
assert oct(chats.state_path().stat().st_mode)[-3:] == "600"
|
|
255
262
|
|
|
256
263
|
|
|
257
264
|
def test_cli_import_chats(cfg, tmp_path, capsys, monkeypatch):
|
|
@@ -303,7 +310,7 @@ def test_downloads_dir_from_xdg_user_dirs(home):
|
|
|
303
310
|
from worklog.config import default_downloads_dir
|
|
304
311
|
assert default_downloads_dir() == home / "Downloads"
|
|
305
312
|
(home / ".config").mkdir(exist_ok=True)
|
|
306
|
-
(home / ".config/user-dirs.dirs").write_text('XDG_DOWNLOAD_DIR="$HOME/Téléchargements"\n')
|
|
313
|
+
(home / ".config/user-dirs.dirs").write_text('XDG_DOWNLOAD_DIR="$HOME/Téléchargements"\n', encoding="utf-8")
|
|
307
314
|
assert default_downloads_dir() == home / "Téléchargements"
|
|
308
315
|
|
|
309
316
|
|
|
@@ -405,3 +412,66 @@ def test_sources_page_and_settings(server, cfg, monkeypatch):
|
|
|
405
412
|
assert new.chat_account == "work" and new.watch_downloads and new.downloads_dir == Path.home() / "dl"
|
|
406
413
|
assert app.post("/sources/settings", {"account": "work", "downloads": "/etc"})[1]
|
|
407
414
|
assert app.post("/sources/settings", {"account": "ghost"})[1]
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# ------------------------------------------------------------------ 0.2.1: summaries, findability, per-chat view
|
|
418
|
+
def test_chat_summary_is_opt_in_cleaned_and_redacted(cfg, tmp_path):
|
|
419
|
+
key = "sk-ant-api03-" + "b" * 30
|
|
420
|
+
summary = f"**Conversation overview**\n\nThe person built a *Trello* sync and pasted {key}. " + "More detail. " * 80
|
|
421
|
+
z = write_zip(tmp_path / "c.zip", [dict(conv("c1", [human("sync my sheet", ts(D, 9))]), summary=summary)])
|
|
422
|
+
path = f"records/{D.isoformat()}/dev1/personal/chat-c1.json"
|
|
423
|
+
chats.import_chats(cfg, z)
|
|
424
|
+
assert "summary" not in records(cfg)[path] # off by default: Claude's text is not stored
|
|
425
|
+
|
|
426
|
+
cfg.keep_chat_summaries = True
|
|
427
|
+
assert chats.import_chats(cfg, z).written == 1 # re-import adds it
|
|
428
|
+
kept = records(cfg)[path]["summary"]
|
|
429
|
+
assert kept.startswith("The person built a Trello sync") and "**" not in kept and "overview" not in kept
|
|
430
|
+
assert key not in kept and "[REDACTED]" in kept
|
|
431
|
+
assert len(kept) <= chats.MAX_SUMMARY_CHARS and kept.endswith("…")
|
|
432
|
+
|
|
433
|
+
cfg.keep_chat_summaries = False
|
|
434
|
+
assert chats.strip_summaries(cfg) == 1 and "summary" not in records(cfg)[path]
|
|
435
|
+
assert chats.strip_summaries(cfg) == 0
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_latest_day_and_chat_days(cfg, tmp_path):
|
|
439
|
+
later = D + timedelta(days=3)
|
|
440
|
+
convs = [conv("c1", [human("a", ts(D, 9))]), conv("c2", [human("b", ts(D, 10)), human("c", ts(later, 11))]),
|
|
441
|
+
conv("c3", [human("d", ts(later, 12))])]
|
|
442
|
+
res = chats.import_chats(cfg, write_zip(tmp_path / "c.zip", convs))
|
|
443
|
+
assert res.latest_day == later and f"Latest chat: {later:%a %d %b %Y}" in res.message()
|
|
444
|
+
assert chats.chat_days(cfg.store_repo) == [(later, 2), (D, 2)]
|
|
445
|
+
assert chats.chat_days(cfg.store_repo, limit=1) == [(later, 2)]
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_day_page_lists_each_chat_and_marks_chat_days(cfg, tmp_path):
|
|
449
|
+
convs = [conv("c1", [human("first ask", ts(D, 9)), human("second ask", ts(D, 9, 5))], name="Trello sync"),
|
|
450
|
+
conv("c2", [human("price question", ts(D, 15))], name="Keel pricing")]
|
|
451
|
+
chats.import_chats(cfg, write_zip(tmp_path / "c.zip", convs))
|
|
452
|
+
body, _ = web.page_day(web.App(8765), cfg, D)
|
|
453
|
+
assert body.index("Trello sync") < body.index("Keel pricing") # in time order, each with its own messages
|
|
454
|
+
assert "first ask" in body and "second ask" in body and "price question" in body
|
|
455
|
+
assert "claude.ai chats</span>" in body and "2 claude.ai chats" in body and "<circle" in body
|
|
456
|
+
later, _ = web.page_day(web.App(8765), cfg, D + timedelta(days=2))
|
|
457
|
+
assert f'href="/?d={D.isoformat()}"' in later and "latest claude.ai chats" in later
|
|
458
|
+
|
|
459
|
+
accounts, projects = aggregate(list(records(cfg).values()))
|
|
460
|
+
text = render(D, cfg, accounts, projects, {})
|
|
461
|
+
assert "- Trello sync: first ask" in text and "- Keel pricing: price question" in text
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def test_turning_summaries_off_in_the_ui_removes_them(server, cfg, monkeypatch):
|
|
465
|
+
app, _ = server
|
|
466
|
+
cfg.keep_chat_summaries = True
|
|
467
|
+
monkeypatch.setattr(web, "save_config", lambda c: None)
|
|
468
|
+
removed = []
|
|
469
|
+
monkeypatch.setattr(chats, "strip_summaries", lambda c: removed.append(c) or 3)
|
|
470
|
+
loc, err, _ = app.post("/sources/settings", {"account": "personal"})
|
|
471
|
+
assert err is None and loc == "/sources?ok=chat_saved" and not cfg.keep_chat_summaries
|
|
472
|
+
for _ in range(100):
|
|
473
|
+
if app.jobs.snapshot()["finished"]:
|
|
474
|
+
break
|
|
475
|
+
time.sleep(0.05)
|
|
476
|
+
assert removed and "Removed claude.ai summaries from 3" in app.jobs.snapshot()["message"]
|
|
477
|
+
assert "Days with chats" not in web.page_sources(app, cfg) # nothing imported yet
|