timing-cli
A command-line interface and MCP server for Timing.app
on macOS. Unlike the Timing Web API — which cannot see your locally recorded app
usage — timing-cli reads Timing's local activity database directly
(read-only) and turns real app usage into aggregated time entries, which it can
then push back to Timing via the Web API.
Because it ships an embedded MCP server, agents (e.g. the Hermes personal agent)
can query your activity and create entries without ever copying or exposing
the raw SQLite.db — the server runs locally, next to the database.
Not affiliated with Timing. Independent tool. It only ever reads the local database; all writes go through the official Web API.
What it does
- Reads the local Timing store (
~/Library/Application Support/info.eurocomp.Timing2/SQLite.db) read-only: automatic app activity, window titles, document paths, projects. - Classifies unassigned activity onto projects with your own rules, plus a packaged default Cognovis ruleset that already covers most of a normal workday (Timing's built-in predicate rules only cover ~15% of activity). It also reuses decoded Timing project predicate rules from the local database when neither user nor default rules already assigned a slice.
- Aggregates consecutive same-project slices into clean time blocks (gap-merging + minimum-duration filtering).
- Pushes the resulting suggestions to Timing as real time entries via the Web API (with a safe dry-run default).
- Serves all of this over MCP (
timing serve) for agents. - Reconstructs bounded read-only work evidence from existing bookings and automatic activity without making a billing decision.
Quickstart
# Install (globally, via uv)
uv tool install timing-cli
# See what's in your local database
timing info
# Daily project summary and suggested entries (read-only)
timing summary --date 2026-07-05
timing suggest --date 2026-07-05
# Reconstruct a synthetic example month as machine-readable evidence
timing reconstruct --month 2026-07 --limit 50 --json
# Push suggestions to Timing (dry-run first, then --yes)
export TIMING_API_KEY=... # from https://web.timingapp.com/integrations/tokens
timing push --date 2026-07-05 # dry-run
timing push --date 2026-07-05 --yes # actually create entries
# Run the MCP server (for Hermes / other agents)
timing serve # stdio
export TIMING_MCP_TOKEN=...
timing serve --transport http # HTTP on 127.0.0.1:8321
# Install a LaunchAgent so the HTTP server auto-starts at login
timing serve --install # requires TIMING_MCP_TOKEN / mcp_http_token
timing serve --uninstall
timing push --yes resolves every non-unassigned suggestion to a unique Web-API
project before creating anything. If a project is ambiguous or unmapped, the push
fails before the first write; add a [project_mappings] override. Re-running the
same push skips matching existing entries unless --replace is passed.
Commands
| Command | Description |
|---|---|
timing info [--json] |
Database location, recorded date range, token status |
timing projects [--remote] [--local-only] [--archived] [--json] |
List projects (local DB or Web API) |
timing usage [--date/--from/--to] [--project ID] [--json] |
Raw automatically tracked app usage |
timing summary [--date/--from/--to] [--json] |
Total time per project |
timing suggest [--date/--from/--to] [--json] |
Aggregated time-entry suggestions (read-only) |
timing push [--date/--from/--to] [--yes] [--replace] [--json] |
Create entries via Web API (dry-run by default) |
| `timing reconstruct (--month YYYY-MM | --from ISO --to ISO) [--limit N] [--cursor TOKEN] [--json]` |
timing serve [--transport] [--host] [--port] |
Run the MCP server |
timing serve --install / --uninstall |
Install/remove a LaunchAgent that runs timing serve --transport http at login |
Machine-readable and non-interactive output
Every command that returns data accepts --json: info returns an object;
projects, usage, summary, and suggest return arrays; push returns an
object containing dry_run, created, skipped, and suggestions; and
reconstruct returns the versioned object documented below. --json changes
the output format only: push remains a dry-run unless --yes is also present.
JSON is written directly to stdout without Rich rendering, tables, or ANSI
codes. Datetimes are ISO-8601 strings, and local SQLite identifiers are decimal
strings so values larger than 2^53 remain lossless. The remote
projects --remote --json path returns the Timing Web API project payload.
projects --local-only makes local access explicit and rejects --remote; the
bounded Executive Pack acceptance resource uses this form.
Human-readable output uses Rich tables. Put the global option before the
subcommand, for example timing --no-color summary; ANSI color is also disabled
automatically whenever stdout is not a TTY. Non-TTY human output remains a
human-oriented table, and no progress indicator or spinner is started there, so
scripts and agents should select --json explicitly.
Window semantics
Date-based CLI queries and MCP tools interpret a local day as the half-open
interval from local midnight to the next local midnight using the system
timezone rules. Daylight-saving transitions therefore change elapsed duration:
in Europe/Berlin, 2026-03-29 runs from 2026-03-29T00:00:00+01:00 to
2026-03-30T00:00:00+02:00 and spans 23 elapsed hours. Adjacent ordinary days
span 24 hours. Explicit offset-aware --from/--to (CLI) or start/end
(MCP) endpoints preserve the instants supplied by the caller.
Daily workflow
# 1. Inspect the day without writing anything.
timing summary --date 2026-07-05
timing suggest --date 2026-07-05
# 2. If push projects are unmapped, inspect remote project references.
export TIMING_API_KEY=...
timing projects --remote
# 3. Add missing [project_mappings] entries, then dry-run again.
timing push --date 2026-07-05
# 4. Create entries once the dry-run looks right.
timing push --date 2026-07-05 --yes
Re-running step 4 for the same day skips matching existing entries. Use
--replace only when you deliberately want Timing's API to replace overlapping
entries in the target window.
Reconstruction evidence contract
timing reconstruct and the MCP reconstruct_work tool share the
timing.reconstruction.v1 response schema. Both are strictly read-only. Supply
either a local calendar month or both endpoints of an explicit half-open window:
timing reconstruct --month 2026-07 --limit 50 --json
timing reconstruct \
--from 2026-07-14T09:00:00+02:00 \
--to 2026-07-14T18:00:00+02:00 \
--limit 50 --json
The equivalent MCP tool arguments are
reconstruct_work(month="2026-07", limit=50, cursor=None).
A booking in that response might identify project Synthetic Studio, carry the
title Synthetic design review, and expose a lossless source ID such as
"18014398509482083".
The top-level fields are:
| Field | Meaning |
|---|---|
schema_version |
Fixed value timing.reconstruction.v1. |
window |
Local timezone, exact [start,end) endpoints, half-open semantics, and extraction timestamp. |
pagination |
Requested limit, stable order (start, source_type, source_id), SQL total/returned counts, completeness, and opaque next_cursor. |
metrics |
Whole-window duration metrics; scope is always window, independent of the returned page. |
records |
At most limit page-scoped booking/activity records with full titles and paths, assignment evidence, relationships, conflicts, and uncovered candidate intervals. |
The maximum page size is 200. Continue with the returned cursor until
pagination.complete is true; this retrieves every source record in the month
without materializing the month as one list. Source rows use SQL keyset paging,
and whole-window calculations scan those rows in bounded batches. A cursor is
bound to a digest of its source-window snapshot; if local Timing data changes,
the continuation fails as stale and must be restarted from the first page.
Relationship lists on a record are also capped at 200 and carry their own *_complete flag,
so dense evidence is never silently presented as complete. Stable source
references have the form booking:<decimal-id> or activity:<decimal-id>.
Ordering uses the clipped source.start, then bookings before activities, then
the decimal source ID. Project title chains retain the 32 most-specific entries
and expose project_title_chain_complete; assignment alternatives are capped at
100 and expose alternatives_complete and conflicts_complete. Per-record
candidate_intervals are capped at 200 and expose
candidate_intervals_complete.
Every local SQLite identifier is serialized as a decimal string on CLI JSON and
MCP boundaries, including identifiers larger than JavaScript's safe integer
range; Python and SQLite continue to use integers internally.
Each assignment includes the current project, selected/proposed project,
origin (existing, user_rule, packaged_rule, timing_predicate, or
unresolved), stable rule identity, matched source field/value, bounded matching
alternatives with completeness metadata, conflict evidence, and an uncertainty
reason when applicable.
Existing assignments keep precedence, but a differently assigned booking stays
visible with its original project and proposed alternatives.
Duration fields are deliberately separate and never imply approval to charge:
| Metric | Scope and definition |
|---|---|
raw_cumulative_activity_seconds |
Window-scoped sum of every clipped automatic-activity duration; overlapping/identical slices count separately. |
activity_interval_union_seconds |
Window-scoped union of automatic-activity intervals; overlaps count once. |
elapsed_evidence_span_seconds |
Window-scoped elapsed time from the first evidence start to the last evidence end. |
gap_seconds |
Window-scoped evidence span not covered by either booking or automatic activity. |
recorded_service_seconds |
Window-scoped union of existing booking intervals; overlapping bookings count once. |
uncovered_candidate_seconds |
Window-scoped union of automatic activity outside every existing booking. It is evidence for review, not an approved charge. |
candidate_intervals[].duration_seconds |
Record-scoped uncovered portion of one automatic-activity source. |
Automatic activity covered by a booking appears as supporting evidence for that
single recorded service. Only uncovered portions appear in candidate_intervals.
Cross-project overlaps are retained as explicit source references rather than
being assigned to whichever row sorts first. The schema contains no rates,
rounding, invoicing, or persistent review decisions.
Configuration
Optional config at ~/.config/timing-cli/config.toml:
# Override the database path if Timing lives elsewhere.
# db_path = "~/Library/Application Support/info.eurocomp.Timing2/SQLite.db"
api_base_url = "https://web.timingapp.com/api/v1"
# api_token = "..." # prefer the TIMING_API_KEY env var instead
# mcp_http_token = "..." # prefer the TIMING_MCP_TOKEN env var instead
min_block_seconds = 120 # drop aggregated blocks shorter than this
gap_merge_seconds = 300 # merge same-project slices split by a gap up to this
# Optional overrides from local Timing projects to Web-API project references.
# Keys can be a local id ("id:42"), a full title chain, or a leaf title.
[project_mappings]
"Client / MIRA" = "/projects/123"
"id:42" = "/projects/456"
# Classification rules: map unassigned activity onto projects.
# First match wins. `app`/`bundle_id` are case-insensitive substrings;
# `title`/`path` are regexes (already matched case-insensitively, so don't
# add an inline `(?i)`).
[[rules]]
project = "MIRA"
title = "polaris"
[[rules]]
project = "Cognovis"
path = "code/mira"
Default classification rules
Even with no config file at all, timing-cli ships a ready-to-use ruleset
tuned for Malte's local setup (timing_cli/default_rules.py,
DEFAULT_COGNOVIS_RULES) that maps common cmux/editor window titles and repo
paths onto real Timing projects:
- MIRA — Polaris/MIRA work: titles matching
polaris|mira|diagnos|isynet| patient|anonymiz|de-id|gc50, or paths containingpolaris/mira. (There is no Timing project literally named "Polaris" — it's tracked as "MIRA".) - Client projects — title keywords for Syntegon, Romelag, MCN, ATR, C4B, NTS, BBW, Kolibri, FUD, Eubylon, Solutio, DST, Agiler Norden.
]project-open[— title/path containingproject-open.- Home Electronic — title/path containing
home-infraoropen-brain. - cognovis Verwaltung — internal tooling/admin keywords (
timing-cli,collmex,paperless,invoice,rechnung,library,beads,claude,codex,agent,skill,acp), repo paths undercode/(cli-tools| library|timing-cli|collmex-cli|mm-cli),.agents, or/skills/, and finally a catch-all for any remainingcmuxactivity.
These defaults are appended after your own [[rules]] (user rules always
win, since the first matching rule wins) and are controlled by:
use_default_rules = true # default; set to false to rely solely on your own [[rules]]
Timing's own project predicates are loaded from the local Project.predicate
column automatically and applied after both explicit config rules and the
packaged defaults. Keep local rules for repository paths, editor titles, and
project-specific conventions that neither Timing nor the packaged defaults
classify well.
Cognovis example
This is a real-world shape for Malte's local setup. Fill the Web-API project
references from timing projects --remote; local project ids can be discovered
with timing projects. With use_default_rules left at its default true,
most of this is already covered — these overrides just fill in
project_mappings and add a couple of extra keywords the defaults don't know
about yet.
api_base_url = "https://web.timingapp.com/api/v1"
min_block_seconds = 120
gap_merge_seconds = 300
[project_mappings]
"MIRA" = "/projects/REMOTE_MIRA"
"cognovis Verwaltung" = "/projects/REMOTE_COGNOVIS_VERWALTUNG"
"]project-open[" = "/projects/REMOTE_PROJECT_OPEN"
"Home Electronic" = "/projects/REMOTE_HOME_ELECTRONIC"
# Extra project-specific keyword not covered by the packaged defaults.
[[rules]]
project = "cognovis Verwaltung"
title = "confluence|jira"
MCP tools
timing serve exposes: list_timing_projects, list_app_usage_tool,
daily_project_summary, suggest_time_entries, create_time_entry (write),
recorded_date_range, and reconstruct_work (read-only, bounded).
HTTP transport requires bearer-token authentication via TIMING_MCP_TOKEN or
mcp_http_token in the config. Stdio transport remains local and does not require
an MCP token.
LaunchAgent (persistent local HTTP server)
timing serve --install writes a LaunchAgent plist
(~/Library/LaunchAgents/de.sussdorff.timing-serve.plist, label
de.sussdorff.timing-serve) that runs timing serve --transport http at login
(RunAtLoad/KeepAlive, LimitLoadToSessionType=Aqua). Logs go to
~/Library/Logs/timing-serve.log / .err.log. Installing requires
TIMING_MCP_TOKEN or mcp_http_token to already be set — the command raises an
error otherwise instead of silently generating a token. timing serve --uninstall
unloads the agent (launchctl bootout) and removes the plist.
Release
The repository includes a GitHub Actions release workflow at
.github/workflows/release.yml. Tag pushes run tests and lint only; published
GitHub releases or manual dispatches build the package and publish to PyPI using
Trusted Publishing. Configure a PyPI trusted publisher for the repository and
the pypi environment before running the publish job.
Requirements
- macOS with Timing.app installed
- Python 3.12+ (installed automatically by
uv tool install) - A Timing Web API token for pushing entries (read-only commands need no token)
License
MIT
Metadata
Release files for timing-cli 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| timing_cli-0.2.0.tar.gz | 43.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| timing_cli-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 88.1 kB
Release files / timing_cli-0.2.0.tar.gz
| Download URL | timing_cli-0.2.0.tar.gz |
|---|---|
| Size | 43.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6b0bb097625a18fb88dbe23f4e17110f254d783aff5e00857512687cd872a3de
|
|
BLAKE2b-256 checksum How to use checksums |
bb9f7a89faeef38f7454194f3b9e39127b0705a0e7ca63e1598580d167046394
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / timing_cli-0.2.0-py3-none-any.whl
| Download URL | timing_cli-0.2.0-py3-none-any.whl |
|---|---|
| Size | 44.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b32a7ba86653956e5de81d35e176536b222a03be00b93e0044737d7f03768446
|
|
BLAKE2b-256 checksum How to use checksums |
38bb64bc9d08ebb3d0e9fe83aec6483e01473d3ee583508d85dbae4ea2d61ea0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|