timing-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- timing_cli-0.1.0/.gitignore +45 -0
- timing_cli-0.1.0/CHANGELOG.md +17 -0
- timing_cli-0.1.0/LICENSE +21 -0
- timing_cli-0.1.0/PKG-INFO +218 -0
- timing_cli-0.1.0/README.md +187 -0
- timing_cli-0.1.0/cliff.toml +51 -0
- timing_cli-0.1.0/pyproject.toml +77 -0
- timing_cli-0.1.0/timing_cli/__init__.py +3 -0
- timing_cli-0.1.0/timing_cli/analysis.py +161 -0
- timing_cli-0.1.0/timing_cli/api.py +311 -0
- timing_cli-0.1.0/timing_cli/cli.py +348 -0
- timing_cli-0.1.0/timing_cli/config.py +92 -0
- timing_cli-0.1.0/timing_cli/db.py +287 -0
- timing_cli-0.1.0/timing_cli/models.py +89 -0
- timing_cli-0.1.0/timing_cli/output.py +78 -0
- timing_cli-0.1.0/timing_cli/rules.py +101 -0
- timing_cli-0.1.0/timing_cli/serve.py +207 -0
- timing_cli-0.1.0/timing_cli/timing_predicates.py +229 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
|
|
12
|
+
# IDE
|
|
13
|
+
.idea/
|
|
14
|
+
.vscode/
|
|
15
|
+
*.swp
|
|
16
|
+
|
|
17
|
+
# Testing
|
|
18
|
+
.coverage
|
|
19
|
+
htmlcov/
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
|
|
22
|
+
# uv
|
|
23
|
+
uv.lock
|
|
24
|
+
|
|
25
|
+
# Claude Code
|
|
26
|
+
.claude
|
|
27
|
+
|
|
28
|
+
# Dolt database files (added by bd init)
|
|
29
|
+
.dolt/
|
|
30
|
+
*.db
|
|
31
|
+
|
|
32
|
+
# Beads JSONL export is local; Dolt is the source of truth.
|
|
33
|
+
.beads/issues.jsonl
|
|
34
|
+
|
|
35
|
+
# Beads runtime artifacts (run state, metrics)
|
|
36
|
+
.beads/runs/
|
|
37
|
+
.beads/export-state.json
|
|
38
|
+
|
|
39
|
+
# Session-close runner artifacts
|
|
40
|
+
.session-close-state.json
|
|
41
|
+
.session-close.lock
|
|
42
|
+
|
|
43
|
+
# Beads / Dolt files (added by bd init)
|
|
44
|
+
.beads-credential-key
|
|
45
|
+
.beads/proxieddb/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
|
|
5
|
+
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Initial scaffold of `timing-cli`.
|
|
12
|
+
- Read-only access to the local Timing.app SQLite database (`db.py`).
|
|
13
|
+
- Rule-based classification of app usage onto projects (`rules.py`).
|
|
14
|
+
- Aggregation of app usage into gap-merged time-entry suggestions (`analysis.py`).
|
|
15
|
+
- Timing Web API client for pushing time entries (`api.py`).
|
|
16
|
+
- Typer CLI: `info`, `projects`, `usage`, `summary`, `suggest`, `push`, `serve`.
|
|
17
|
+
- FastMCP server exposing local activity and entry creation to agents (`serve.py`).
|
timing_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Malte Sussdorff
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: timing-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI + MCP server for the Timing.app macOS time tracker - reads the local activity database and generates/pushes time entries via the Timing Web API
|
|
5
|
+
Project-URL: Homepage, https://github.com/sussdorff/timing-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/sussdorff/timing-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/sussdorff/timing-cli/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/sussdorff/timing-cli/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Malte Sussdorff
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cli,macos,mcp,productivity,time-tracking,timing
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Office/Business
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: fastmcp>=2.0
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Requires-Dist: mcp>=1.23.0
|
|
27
|
+
Requires-Dist: pydantic>=2.0.0
|
|
28
|
+
Requires-Dist: rich>=13.0.0
|
|
29
|
+
Requires-Dist: typer>=0.15.0
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# timing-cli
|
|
33
|
+
|
|
34
|
+
A command-line interface **and MCP server** for [Timing.app](https://timingapp.com/)
|
|
35
|
+
on macOS. Unlike the Timing Web API — which cannot see your locally recorded app
|
|
36
|
+
usage — `timing-cli` reads Timing's **local activity database directly**
|
|
37
|
+
(read-only) and turns real app usage into aggregated time entries, which it can
|
|
38
|
+
then push back to Timing via the Web API.
|
|
39
|
+
|
|
40
|
+
Because it ships an embedded MCP server, agents (e.g. the Hermes personal agent)
|
|
41
|
+
can query your activity and create entries **without ever copying or exposing
|
|
42
|
+
the raw `SQLite.db`** — the server runs locally, next to the database.
|
|
43
|
+
|
|
44
|
+
> **Not affiliated with Timing.** Independent tool. It only ever *reads* the
|
|
45
|
+
> local database; all writes go through the official Web API.
|
|
46
|
+
|
|
47
|
+
## What it does
|
|
48
|
+
|
|
49
|
+
- **Reads** the local Timing store (`~/Library/Application Support/info.eurocomp.Timing2/SQLite.db`)
|
|
50
|
+
read-only: automatic app activity, window titles, document paths, projects.
|
|
51
|
+
- **Classifies** unassigned activity onto projects with your own rules (Timing's
|
|
52
|
+
built-in predicate rules only cover ~15% of activity). It also reuses decoded
|
|
53
|
+
Timing project predicate rules from the local database when Timing did not
|
|
54
|
+
already assign a slice.
|
|
55
|
+
- **Aggregates** consecutive same-project slices into clean time blocks
|
|
56
|
+
(gap-merging + minimum-duration filtering).
|
|
57
|
+
- **Pushes** the resulting suggestions to Timing as real time entries via the
|
|
58
|
+
Web API (with a safe dry-run default).
|
|
59
|
+
- **Serves** all of this over MCP (`timing serve`) for agents.
|
|
60
|
+
|
|
61
|
+
## Quickstart
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Install (globally, via uv)
|
|
65
|
+
uv tool install timing-cli
|
|
66
|
+
|
|
67
|
+
# See what's in your local database
|
|
68
|
+
timing info
|
|
69
|
+
|
|
70
|
+
# Daily project summary and suggested entries (read-only)
|
|
71
|
+
timing summary --date 2026-07-05
|
|
72
|
+
timing suggest --date 2026-07-05
|
|
73
|
+
|
|
74
|
+
# Push suggestions to Timing (dry-run first, then --yes)
|
|
75
|
+
export TIMING_API_KEY=... # from https://web.timingapp.com/integrations/tokens
|
|
76
|
+
timing push --date 2026-07-05 # dry-run
|
|
77
|
+
timing push --date 2026-07-05 --yes # actually create entries
|
|
78
|
+
|
|
79
|
+
# Run the MCP server (for Hermes / other agents)
|
|
80
|
+
timing serve # stdio
|
|
81
|
+
export TIMING_MCP_TOKEN=...
|
|
82
|
+
timing serve --transport http # HTTP on 127.0.0.1:8321
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`timing push --yes` resolves every non-unassigned suggestion to a unique Web-API
|
|
86
|
+
project before creating anything. If a project is ambiguous or unmapped, the push
|
|
87
|
+
fails before the first write; add a `[project_mappings]` override. Re-running the
|
|
88
|
+
same push skips matching existing entries unless `--replace` is passed.
|
|
89
|
+
|
|
90
|
+
## Commands
|
|
91
|
+
|
|
92
|
+
| Command | Description |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `timing info` | Database location, recorded date range, token status |
|
|
95
|
+
| `timing projects [--remote] [--archived]` | List projects (local DB or Web API) |
|
|
96
|
+
| `timing usage [--date/--from/--to] [--project ID]` | Raw automatically tracked app usage |
|
|
97
|
+
| `timing summary [--date/--from/--to]` | Total time per project |
|
|
98
|
+
| `timing suggest [--date/--from/--to]` | Aggregated time-entry suggestions (read-only) |
|
|
99
|
+
| `timing push [--date/--from/--to] [--yes] [--replace]` | Create entries via Web API (dry-run by default) |
|
|
100
|
+
| `timing serve [--transport] [--host] [--port]` | Run the MCP server |
|
|
101
|
+
|
|
102
|
+
## Daily workflow
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# 1. Inspect the day without writing anything.
|
|
106
|
+
timing summary --date 2026-07-05
|
|
107
|
+
timing suggest --date 2026-07-05
|
|
108
|
+
|
|
109
|
+
# 2. If push projects are unmapped, inspect remote project references.
|
|
110
|
+
export TIMING_API_KEY=...
|
|
111
|
+
timing projects --remote
|
|
112
|
+
|
|
113
|
+
# 3. Add missing [project_mappings] entries, then dry-run again.
|
|
114
|
+
timing push --date 2026-07-05
|
|
115
|
+
|
|
116
|
+
# 4. Create entries once the dry-run looks right.
|
|
117
|
+
timing push --date 2026-07-05 --yes
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Re-running step 4 for the same day skips matching existing entries. Use
|
|
121
|
+
`--replace` only when you deliberately want Timing's API to replace overlapping
|
|
122
|
+
entries in the target window.
|
|
123
|
+
|
|
124
|
+
## Configuration
|
|
125
|
+
|
|
126
|
+
Optional config at `~/.config/timing-cli/config.toml`:
|
|
127
|
+
|
|
128
|
+
```toml
|
|
129
|
+
# Override the database path if Timing lives elsewhere.
|
|
130
|
+
# db_path = "~/Library/Application Support/info.eurocomp.Timing2/SQLite.db"
|
|
131
|
+
|
|
132
|
+
api_base_url = "https://web.timingapp.com/api/v1"
|
|
133
|
+
# api_token = "..." # prefer the TIMING_API_KEY env var instead
|
|
134
|
+
# mcp_http_token = "..." # prefer the TIMING_MCP_TOKEN env var instead
|
|
135
|
+
|
|
136
|
+
min_block_seconds = 120 # drop aggregated blocks shorter than this
|
|
137
|
+
gap_merge_seconds = 300 # merge same-project slices split by a gap up to this
|
|
138
|
+
|
|
139
|
+
# Optional overrides from local Timing projects to Web-API project references.
|
|
140
|
+
# Keys can be a local id ("id:42"), a full title chain, or a leaf title.
|
|
141
|
+
[project_mappings]
|
|
142
|
+
"Client / Polaris" = "/projects/123"
|
|
143
|
+
"id:42" = "/projects/456"
|
|
144
|
+
|
|
145
|
+
# Classification rules: map unassigned activity onto projects.
|
|
146
|
+
# First match wins. `app`/`bundle_id` are case-insensitive substrings;
|
|
147
|
+
# `title`/`path` are regexes.
|
|
148
|
+
[[rules]]
|
|
149
|
+
project = "Polaris"
|
|
150
|
+
title = "polaris"
|
|
151
|
+
|
|
152
|
+
[[rules]]
|
|
153
|
+
project = "Cognovis"
|
|
154
|
+
path = "code/mira"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Cognovis example
|
|
158
|
+
|
|
159
|
+
This is a real-world shape for Malte's local setup. Fill the Web-API project
|
|
160
|
+
references from `timing projects --remote`; local project ids can be discovered
|
|
161
|
+
with `timing projects`.
|
|
162
|
+
|
|
163
|
+
```toml
|
|
164
|
+
api_base_url = "https://web.timingapp.com/api/v1"
|
|
165
|
+
min_block_seconds = 120
|
|
166
|
+
gap_merge_seconds = 300
|
|
167
|
+
|
|
168
|
+
[project_mappings]
|
|
169
|
+
"cognovis Verwaltung" = "/projects/REMOTE_COGNOVIS_VERWALTUNG"
|
|
170
|
+
"]project-open[" = "/projects/REMOTE_PROJECT_OPEN"
|
|
171
|
+
"Home Electronic" = "/projects/REMOTE_HOME_ELECTRONIC"
|
|
172
|
+
|
|
173
|
+
[[rules]]
|
|
174
|
+
project = "cognovis Verwaltung"
|
|
175
|
+
title = "(timing|collmex|paperless|invoice|rechnung)"
|
|
176
|
+
|
|
177
|
+
[[rules]]
|
|
178
|
+
project = "cognovis Verwaltung"
|
|
179
|
+
path = "code/(cli-tools|library)"
|
|
180
|
+
|
|
181
|
+
[[rules]]
|
|
182
|
+
project = "]project-open["
|
|
183
|
+
app = "Mail"
|
|
184
|
+
title = "project-open"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Timing's own project predicates are loaded from the local `Project.predicate`
|
|
188
|
+
column automatically and applied after explicit config rules. Keep local rules
|
|
189
|
+
for repository paths, editor titles, and project-specific conventions that
|
|
190
|
+
Timing itself does not classify well.
|
|
191
|
+
|
|
192
|
+
## MCP tools
|
|
193
|
+
|
|
194
|
+
`timing serve` exposes: `list_timing_projects`, `list_app_usage_tool`,
|
|
195
|
+
`daily_project_summary`, `suggest_time_entries`, `create_time_entry` (write),
|
|
196
|
+
`recorded_date_range`.
|
|
197
|
+
|
|
198
|
+
HTTP transport requires bearer-token authentication via `TIMING_MCP_TOKEN` or
|
|
199
|
+
`mcp_http_token` in the config. Stdio transport remains local and does not require
|
|
200
|
+
an MCP token.
|
|
201
|
+
|
|
202
|
+
## Release
|
|
203
|
+
|
|
204
|
+
The repository includes a GitHub Actions release workflow at
|
|
205
|
+
`.github/workflows/release.yml`. Tag pushes run tests and lint only; published
|
|
206
|
+
GitHub releases or manual dispatches build the package and publish to PyPI using
|
|
207
|
+
Trusted Publishing. Configure a PyPI trusted publisher for the repository and
|
|
208
|
+
the `pypi` environment before running the publish job.
|
|
209
|
+
|
|
210
|
+
## Requirements
|
|
211
|
+
|
|
212
|
+
- macOS with Timing.app installed
|
|
213
|
+
- Python 3.12+ (installed automatically by `uv tool install`)
|
|
214
|
+
- A Timing Web API token for pushing entries (read-only commands need no token)
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# timing-cli
|
|
2
|
+
|
|
3
|
+
A command-line interface **and MCP server** for [Timing.app](https://timingapp.com/)
|
|
4
|
+
on macOS. Unlike the Timing Web API — which cannot see your locally recorded app
|
|
5
|
+
usage — `timing-cli` reads Timing's **local activity database directly**
|
|
6
|
+
(read-only) and turns real app usage into aggregated time entries, which it can
|
|
7
|
+
then push back to Timing via the Web API.
|
|
8
|
+
|
|
9
|
+
Because it ships an embedded MCP server, agents (e.g. the Hermes personal agent)
|
|
10
|
+
can query your activity and create entries **without ever copying or exposing
|
|
11
|
+
the raw `SQLite.db`** — the server runs locally, next to the database.
|
|
12
|
+
|
|
13
|
+
> **Not affiliated with Timing.** Independent tool. It only ever *reads* the
|
|
14
|
+
> local database; all writes go through the official Web API.
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
- **Reads** the local Timing store (`~/Library/Application Support/info.eurocomp.Timing2/SQLite.db`)
|
|
19
|
+
read-only: automatic app activity, window titles, document paths, projects.
|
|
20
|
+
- **Classifies** unassigned activity onto projects with your own rules (Timing's
|
|
21
|
+
built-in predicate rules only cover ~15% of activity). It also reuses decoded
|
|
22
|
+
Timing project predicate rules from the local database when Timing did not
|
|
23
|
+
already assign a slice.
|
|
24
|
+
- **Aggregates** consecutive same-project slices into clean time blocks
|
|
25
|
+
(gap-merging + minimum-duration filtering).
|
|
26
|
+
- **Pushes** the resulting suggestions to Timing as real time entries via the
|
|
27
|
+
Web API (with a safe dry-run default).
|
|
28
|
+
- **Serves** all of this over MCP (`timing serve`) for agents.
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Install (globally, via uv)
|
|
34
|
+
uv tool install timing-cli
|
|
35
|
+
|
|
36
|
+
# See what's in your local database
|
|
37
|
+
timing info
|
|
38
|
+
|
|
39
|
+
# Daily project summary and suggested entries (read-only)
|
|
40
|
+
timing summary --date 2026-07-05
|
|
41
|
+
timing suggest --date 2026-07-05
|
|
42
|
+
|
|
43
|
+
# Push suggestions to Timing (dry-run first, then --yes)
|
|
44
|
+
export TIMING_API_KEY=... # from https://web.timingapp.com/integrations/tokens
|
|
45
|
+
timing push --date 2026-07-05 # dry-run
|
|
46
|
+
timing push --date 2026-07-05 --yes # actually create entries
|
|
47
|
+
|
|
48
|
+
# Run the MCP server (for Hermes / other agents)
|
|
49
|
+
timing serve # stdio
|
|
50
|
+
export TIMING_MCP_TOKEN=...
|
|
51
|
+
timing serve --transport http # HTTP on 127.0.0.1:8321
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`timing push --yes` resolves every non-unassigned suggestion to a unique Web-API
|
|
55
|
+
project before creating anything. If a project is ambiguous or unmapped, the push
|
|
56
|
+
fails before the first write; add a `[project_mappings]` override. Re-running the
|
|
57
|
+
same push skips matching existing entries unless `--replace` is passed.
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
| Command | Description |
|
|
62
|
+
| --- | --- |
|
|
63
|
+
| `timing info` | Database location, recorded date range, token status |
|
|
64
|
+
| `timing projects [--remote] [--archived]` | List projects (local DB or Web API) |
|
|
65
|
+
| `timing usage [--date/--from/--to] [--project ID]` | Raw automatically tracked app usage |
|
|
66
|
+
| `timing summary [--date/--from/--to]` | Total time per project |
|
|
67
|
+
| `timing suggest [--date/--from/--to]` | Aggregated time-entry suggestions (read-only) |
|
|
68
|
+
| `timing push [--date/--from/--to] [--yes] [--replace]` | Create entries via Web API (dry-run by default) |
|
|
69
|
+
| `timing serve [--transport] [--host] [--port]` | Run the MCP server |
|
|
70
|
+
|
|
71
|
+
## Daily workflow
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# 1. Inspect the day without writing anything.
|
|
75
|
+
timing summary --date 2026-07-05
|
|
76
|
+
timing suggest --date 2026-07-05
|
|
77
|
+
|
|
78
|
+
# 2. If push projects are unmapped, inspect remote project references.
|
|
79
|
+
export TIMING_API_KEY=...
|
|
80
|
+
timing projects --remote
|
|
81
|
+
|
|
82
|
+
# 3. Add missing [project_mappings] entries, then dry-run again.
|
|
83
|
+
timing push --date 2026-07-05
|
|
84
|
+
|
|
85
|
+
# 4. Create entries once the dry-run looks right.
|
|
86
|
+
timing push --date 2026-07-05 --yes
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Re-running step 4 for the same day skips matching existing entries. Use
|
|
90
|
+
`--replace` only when you deliberately want Timing's API to replace overlapping
|
|
91
|
+
entries in the target window.
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
Optional config at `~/.config/timing-cli/config.toml`:
|
|
96
|
+
|
|
97
|
+
```toml
|
|
98
|
+
# Override the database path if Timing lives elsewhere.
|
|
99
|
+
# db_path = "~/Library/Application Support/info.eurocomp.Timing2/SQLite.db"
|
|
100
|
+
|
|
101
|
+
api_base_url = "https://web.timingapp.com/api/v1"
|
|
102
|
+
# api_token = "..." # prefer the TIMING_API_KEY env var instead
|
|
103
|
+
# mcp_http_token = "..." # prefer the TIMING_MCP_TOKEN env var instead
|
|
104
|
+
|
|
105
|
+
min_block_seconds = 120 # drop aggregated blocks shorter than this
|
|
106
|
+
gap_merge_seconds = 300 # merge same-project slices split by a gap up to this
|
|
107
|
+
|
|
108
|
+
# Optional overrides from local Timing projects to Web-API project references.
|
|
109
|
+
# Keys can be a local id ("id:42"), a full title chain, or a leaf title.
|
|
110
|
+
[project_mappings]
|
|
111
|
+
"Client / Polaris" = "/projects/123"
|
|
112
|
+
"id:42" = "/projects/456"
|
|
113
|
+
|
|
114
|
+
# Classification rules: map unassigned activity onto projects.
|
|
115
|
+
# First match wins. `app`/`bundle_id` are case-insensitive substrings;
|
|
116
|
+
# `title`/`path` are regexes.
|
|
117
|
+
[[rules]]
|
|
118
|
+
project = "Polaris"
|
|
119
|
+
title = "polaris"
|
|
120
|
+
|
|
121
|
+
[[rules]]
|
|
122
|
+
project = "Cognovis"
|
|
123
|
+
path = "code/mira"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Cognovis example
|
|
127
|
+
|
|
128
|
+
This is a real-world shape for Malte's local setup. Fill the Web-API project
|
|
129
|
+
references from `timing projects --remote`; local project ids can be discovered
|
|
130
|
+
with `timing projects`.
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
api_base_url = "https://web.timingapp.com/api/v1"
|
|
134
|
+
min_block_seconds = 120
|
|
135
|
+
gap_merge_seconds = 300
|
|
136
|
+
|
|
137
|
+
[project_mappings]
|
|
138
|
+
"cognovis Verwaltung" = "/projects/REMOTE_COGNOVIS_VERWALTUNG"
|
|
139
|
+
"]project-open[" = "/projects/REMOTE_PROJECT_OPEN"
|
|
140
|
+
"Home Electronic" = "/projects/REMOTE_HOME_ELECTRONIC"
|
|
141
|
+
|
|
142
|
+
[[rules]]
|
|
143
|
+
project = "cognovis Verwaltung"
|
|
144
|
+
title = "(timing|collmex|paperless|invoice|rechnung)"
|
|
145
|
+
|
|
146
|
+
[[rules]]
|
|
147
|
+
project = "cognovis Verwaltung"
|
|
148
|
+
path = "code/(cli-tools|library)"
|
|
149
|
+
|
|
150
|
+
[[rules]]
|
|
151
|
+
project = "]project-open["
|
|
152
|
+
app = "Mail"
|
|
153
|
+
title = "project-open"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Timing's own project predicates are loaded from the local `Project.predicate`
|
|
157
|
+
column automatically and applied after explicit config rules. Keep local rules
|
|
158
|
+
for repository paths, editor titles, and project-specific conventions that
|
|
159
|
+
Timing itself does not classify well.
|
|
160
|
+
|
|
161
|
+
## MCP tools
|
|
162
|
+
|
|
163
|
+
`timing serve` exposes: `list_timing_projects`, `list_app_usage_tool`,
|
|
164
|
+
`daily_project_summary`, `suggest_time_entries`, `create_time_entry` (write),
|
|
165
|
+
`recorded_date_range`.
|
|
166
|
+
|
|
167
|
+
HTTP transport requires bearer-token authentication via `TIMING_MCP_TOKEN` or
|
|
168
|
+
`mcp_http_token` in the config. Stdio transport remains local and does not require
|
|
169
|
+
an MCP token.
|
|
170
|
+
|
|
171
|
+
## Release
|
|
172
|
+
|
|
173
|
+
The repository includes a GitHub Actions release workflow at
|
|
174
|
+
`.github/workflows/release.yml`. Tag pushes run tests and lint only; published
|
|
175
|
+
GitHub releases or manual dispatches build the package and publish to PyPI using
|
|
176
|
+
Trusted Publishing. Configure a PyPI trusted publisher for the repository and
|
|
177
|
+
the `pypi` environment before running the publish job.
|
|
178
|
+
|
|
179
|
+
## Requirements
|
|
180
|
+
|
|
181
|
+
- macOS with Timing.app installed
|
|
182
|
+
- Python 3.12+ (installed automatically by `uv tool install`)
|
|
183
|
+
- A Timing Web API token for pushing entries (read-only commands need no token)
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[changelog]
|
|
2
|
+
header = """
|
|
3
|
+
# Changelog
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
body = """
|
|
7
|
+
{% if version %}
|
|
8
|
+
## [{{ version }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
9
|
+
{% else %}
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
{% endif %}
|
|
12
|
+
|
|
13
|
+
{% for group, commits in commits | group_by(attribute="group") %}
|
|
14
|
+
### {{ group }}
|
|
15
|
+
{% for commit in commits %}
|
|
16
|
+
- {% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | upper_first }}
|
|
17
|
+
{% endfor %}
|
|
18
|
+
{% endfor %}
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
trim = true
|
|
22
|
+
|
|
23
|
+
[git]
|
|
24
|
+
conventional_commits = true
|
|
25
|
+
filter_unconventional = false
|
|
26
|
+
split_commits = false
|
|
27
|
+
protect_breaking_commits = true
|
|
28
|
+
filter_commits = true
|
|
29
|
+
tag_pattern = "^(?:v)?(?:\\d{4}\\.\\d{1,2}\\.\\d{1,2}(?:\\.\\d+)?|\\d+\\.\\d+\\.\\d+)$"
|
|
30
|
+
skip_tags = ""
|
|
31
|
+
ignore_tags = ""
|
|
32
|
+
topo_order = false
|
|
33
|
+
sort_commits = "oldest"
|
|
34
|
+
commit_preprocessors = [
|
|
35
|
+
{ pattern = "^Merge .*", replace = "" },
|
|
36
|
+
]
|
|
37
|
+
commit_parsers = [
|
|
38
|
+
{ message = "^feat", group = "Added" },
|
|
39
|
+
{ message = "^fix", group = "Fixed" },
|
|
40
|
+
{ message = "^perf", group = "Changed" },
|
|
41
|
+
{ message = "^refactor", group = "Changed" },
|
|
42
|
+
{ message = "^docs", group = "Documentation" },
|
|
43
|
+
{ message = "^test", group = "Tests" },
|
|
44
|
+
{ message = "^build", group = "Build" },
|
|
45
|
+
{ message = "^ci", group = "Build" },
|
|
46
|
+
{ message = "^chore", group = "Changed" },
|
|
47
|
+
{ message = "^revert", group = "Changed" },
|
|
48
|
+
{ message = "^bd sync:", skip = true },
|
|
49
|
+
{ message = "^bd:", skip = true },
|
|
50
|
+
{ message = ".*", group = "Changed" },
|
|
51
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "timing-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "CLI + MCP server for the Timing.app macOS time tracker - reads the local activity database and generates/pushes time entries via the Timing Web API"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Malte Sussdorff" },
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
keywords = ["timing", "time-tracking", "cli", "mcp", "macos", "productivity"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Intended Audience :: End Users/Desktop",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: MacOS",
|
|
18
|
+
"Operating System :: MacOS :: MacOS X",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Office/Business",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"typer>=0.15.0",
|
|
27
|
+
"rich>=13.0.0",
|
|
28
|
+
"fastmcp>=2.0",
|
|
29
|
+
"mcp>=1.23.0",
|
|
30
|
+
"httpx>=0.27.0",
|
|
31
|
+
"pydantic>=2.0.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
timing = "timing_cli.cli:app"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/sussdorff/timing-cli"
|
|
39
|
+
Repository = "https://github.com/sussdorff/timing-cli"
|
|
40
|
+
Issues = "https://github.com/sussdorff/timing-cli/issues"
|
|
41
|
+
Changelog = "https://github.com/sussdorff/timing-cli/blob/main/CHANGELOG.md"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["hatchling"]
|
|
45
|
+
build-backend = "hatchling.build"
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel]
|
|
48
|
+
packages = ["timing_cli"]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
include = [
|
|
52
|
+
"/timing_cli",
|
|
53
|
+
"/README.md",
|
|
54
|
+
"/CHANGELOG.md",
|
|
55
|
+
"/LICENSE",
|
|
56
|
+
"/cliff.toml",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[dependency-groups]
|
|
60
|
+
dev = [
|
|
61
|
+
"git-cliff>=2.12.0",
|
|
62
|
+
"pytest>=8.0.0",
|
|
63
|
+
"pytest-cov>=6.0.0",
|
|
64
|
+
"ruff>=0.8.0",
|
|
65
|
+
"bandit>=1.8.0",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.ruff]
|
|
69
|
+
target-version = "py312"
|
|
70
|
+
line-length = 100
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint]
|
|
73
|
+
select = ["E", "F", "I", "UP", "B", "C4"]
|
|
74
|
+
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
testpaths = ["tests"]
|
|
77
|
+
addopts = "-v"
|