Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Bug report
description: The status line shows something wrong, or the installer failed.
labels: ["bug"]
body:
- type: textarea
id: what-happened
attributes:
label: What happened
description: What the status line showed, or what the installer printed.
placeholder: The countdown showed 58:12 on a session I had left idle overnight.
validations:
required: true

- type: textarea
id: expected
attributes:
label: What you expected instead
validations:
required: true

- type: input
id: os
attributes:
label: Operating system
description: >
Include whether you are in WSL. WSL and Windows have separate home
directories and need separate installs.
placeholder: Windows 11, running Claude Code inside WSL2 (Ubuntu 24.04)
validations:
required: true

- type: input
id: python
attributes:
label: Python version
description: Output of `python3 --version`.
placeholder: "3.11.9"
validations:
required: true

- type: input
id: claude-code
attributes:
label: Claude Code version
description: Output of `claude --version`.
placeholder: "2.1.220"
validations:
required: true

- type: dropdown
id: install-method
attributes:
label: How you installed it
options:
- uv tool install
- pipx install
- from a clone, without installing
- other (say which below)
validations:
required: true

- type: textarea
id: settings
attributes:
label: Your statusLine block
description: >
The `statusLine` key from `~/.claude/settings.json`. Paste the block
only, not the whole file, which may hold API keys or other settings you
would rather not publish.
render: json
placeholder: |
"statusLine": {
"type": "command",
"command": "claude-cache-timer",
"refreshInterval": 1
}
validations:
required: true

- type: textarea
id: extra
attributes:
label: Anything else
description: >
Your terminal emulator, if the problem is glyphs or colour. Relevant
transcript lines, if the countdown itself is wrong. Redact those first:
transcripts contain your conversation.
validations:
required: false
12 changes: 12 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
blank_issues_enabled: false
contact_links:
- name: How the timer knows what it knows
url: https://github.com/GabrielAndreiPreda/claude-code-cache-timer/blob/main/CACHE-MECHANISM.md
about: >
Where the number comes from, why the clock is the last assistant turn,
and why there are no hooks.
- name: Known limitations
url: https://github.com/GabrielAndreiPreda/claude-code-cache-timer/blob/main/README.md#limitations
about: >
The status line being hidden during permission prompts, and the countdown
reading slightly optimistic, are both already documented.
29 changes: 29 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Feature request
description: Suggest something the status line should do differently.
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: What are you trying to work out that the display does not tell you
description: Describe the situation rather than the feature.
validations:
required: true

- type: textarea
id: proposal
attributes:
label: What you have in mind
validations:
required: false

- type: textarea
id: extra
attributes:
label: Anything else
description: >
The command runs once per second in every open session, so cost on the
render path is worth mentioning if your suggestion affects it. See
CONTRIBUTING.md.
validations:
required: false
9 changes: 9 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## What this changes

<!-- And why. If it changes what the row displays, include a before and after. -->

## Checks

- [ ] `python3 -m unittest discover -s tests` passes locally
- [ ] Nothing new on the render path can raise or write to stderr
- [ ] Comments still match the decisions they document
64 changes: 64 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Tests

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

# The status line has no runtime dependencies and CI needs nothing from the API.
permissions:
contents: read

jobs:
test:
name: Python ${{ matrix.python }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
# Report every platform, rather than stopping at the first failure.
fail-fast: false
matrix:
include:
# setup-python has no 3.8 build for the 24.04 image ubuntu-latest now
# resolves to, so the floor declared in pyproject.toml is tested on a
# pinned 22.04 runner.
- { os: ubuntu-22.04, python: "3.8" }
- { os: ubuntu-latest, python: "3.9" }
- { os: ubuntu-latest, python: "3.11" }
- { os: ubuntu-latest, python: "3.13" }
# Windows covers the code page and path separator differences.
- { os: windows-latest, python: "3.8" }
- { os: windows-latest, python: "3.13" }
- { os: macos-latest, python: "3.13" }
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python }}

- name: Run tests
run: python -m unittest discover -s tests -v

build:
name: Build the package
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-python@v7
with:
python-version: "3.13"

- name: Build wheel and sdist
run: |
python -m pip install --upgrade build twine
python -m build
# Catches metadata that is valid TOML but invalid packaging.
python -m twine check dist/*

- name: Install the wheel and run the console script
run: |
python -m pip install dist/*.whl
claude-cache-timer --version
claude-cache-timer --help
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ venv/
build/
dist/
*.egg-info/

# Per-machine Claude Code permissions. A shared .claude/settings.json stays trackable.
.claude/settings.local.json
28 changes: 11 additions & 17 deletions CACHE-MECHANISM.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ request went out.

A slash command is the clearest case: running `/exit` or `/clear` appends a timestamped
record and makes no request at all, so a clock keyed to the newest record restarts on a
cache nothing touched. Your own turn is the same problem in slower motion it is
cache nothing touched. Your own turn is the same problem in slower motion: it is
written when you hit enter, before the request it will eventually trigger.

The marker that does hold is `message.usage`. Only assistant turns carry one, and only
Expand Down Expand Up @@ -85,9 +85,9 @@ find the TTL, and one backwards walk answers both questions.
This compares all three clocks across every transcript you have. A row where mtime
disagrees by hours is a session an mtime countdown would have lied about; a non-zero
last column is a session the newest-record countdown would have lied about, by that
many seconds of cache already spent. Sessions that ended on a local record — which is
common, since local records are written after the last response — are the ones to look
at.
many seconds of cache already spent. The rows to look at are the sessions that ended on a
local record, which is the common case, since local records are written after the last
response.

```sh
python3 - <<'EOF'
Expand Down Expand Up @@ -136,24 +136,21 @@ The filesystem layout makes this self-solving:
Subagent writes land in a subdirectory, so they add no record to the parent file. Read the
parent and the stall is already reflected.


## Reading the TTL


Assistant records carry the answer under `message.usage.cache_creation`:

```json
"cache_creation": { "ephemeral_1h_input_tokens": 19627, "ephemeral_5m_input_tokens": 0 }
```

Whichever bucket is non-zero is the TTL that was written. Three details matter:
Whichever bucket is non-zero is the TTL that was written. Three details matter.

A turn that only reads the cache records `{0, 0}` in both buckets. Those records are not
an answer and have to be skipped, not treated as "unknown".

Sessions can move between TTLs mid-run.
Re-read the value every tick rather than caching it once at startup.

Sessions can move between TTLs mid-run, so the value has to be re-read every tick rather
than cached once at startup.

If no non-zero bucket exists anywhere in the tail, the TTL is genuinely unknown and the
display says so. One exception: past 3600 seconds of inactivity the session is cold no
Expand All @@ -180,10 +177,10 @@ addition to the event-driven updates". The docs recommend it for exactly this ca
based data, or a main session sitting idle while background subagents work.

Without `refreshInterval` the command runs only when a new assistant message arrives, when
`/compact` finishes, when the permission mode changes, and when vim mode toggles.
`/compact` finishes, when the permission mode changes, and when vim mode toggles.

The status line is hidden during permission dialogs, autocomplete, and the help menu.
This cannot be changed from inside the status line.
The status line is hidden during permission dialogs, autocomplete, and the help menu, and
nothing inside the status line can change that.

## Why there are no hooks

Expand All @@ -199,7 +196,6 @@ terminal emulator, returning 0. Every session then matches every other session.
tmux, in containers, and over SSH the same thing happens for different reasons. Any
cleanup logic keyed on PID deletes live sessions' state.


## Why this is not a plugin

Plugins can ship skills, agents, hooks, MCP servers, LSP servers, and monitors. A plugin
Expand All @@ -220,6 +216,4 @@ noise. Against five minutes on a slow turn it is worth knowing.
The countdown rounds up, so the display reads `0:01` through the final second rather than
sitting at `0:00` while the cache is still alive.

During permission prompts the number is invisible, as described
above.

During permission prompts the number is invisible, as described above.
87 changes: 87 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Changelog

Notable changes to this project. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
[semantic versioning](https://semver.org/spec/v2.0.0.html).

Releases before 2.1.0 were tagged retroactively, so their dates come from the commits.

## [Unreleased]

Nothing yet.

## [2.1.0] - 2026-08-12

The encoding fix below was committed on 2026-08-10 but never released: `__version__` was
left at `2.0.0`, so no package reported itself as 2.1.0. This release is the first that
does.

### Fixed

- Install verification reported a working command as broken on Windows. It read the child
process's output using the system's preferred encoding, which on a legacy code page
cannot represent the hourglass. The failure happened inside a reader thread, so it
neither propagated nor filled the buffer, and verification saw empty output. The reader
now decodes UTF-8 explicitly, matching what the status line writes.
- `__version__` still read `2.0.0`.
- A test compared `candidate().argv[0]` against a literal POSIX path, but that value goes
through `os.path.abspath`, which rewrites it on Windows. The test could not pass there.
- `test_real_repository` compared `git rev-parse --abbrev-ref HEAD` against `git_branch`,
which cannot hold on a detached HEAD, where the former answers the literal string `HEAD`
and the latter answers a short SHA. It now resolves the SHA, so the detached case is
tested against real git rather than only a fixture.
- The README documented a status line wrapping mode that was removed in 2.0.0.
- `.claude/settings.local.json` is now ignored by the repository rather than relying on a
contributor's global git configuration.

### Added

- Continuous integration on Linux, macOS and Windows across Python 3.8 through 3.13, plus a
job that builds the package, checks its metadata, installs the wheel and runs the console
script.
- `CONTRIBUTING.md`, issue and pull request templates, and this changelog.
- Package metadata: author, issue and changelog URLs, and per-version Python classifiers.
- A `--version` flag entry in the README options table, and instructions for pinning an
install to a tag, since installing from `main` gave no way to hold a version.

### Changed

- The README is reorganised so the install command is reachable without scrolling, and the
longer explanations of PATH, shell quoting and `settings.json` parsing moved into
collapsible sections.
- `image.png` is now `docs/statusline.png`.

## [2.0.0] - 2026-08-10

### Changed

- Installation moved to `uv tool install` or `pipx install`, and the package gained a
`claude-cache-timer` console script. The status line command written into
`settings.json` is now that bare name.
- Shell detection is gone. A bare name has no path separators, no spaces and nothing any
shell treats specially, so it runs identically whether Claude Code routes the status
line through Git Bash, PowerShell or `sh`, and the installer never has to work out
which.
- The code moved to a `src/cache_timer/` package with separate `cli`, `statusline` and
`install` modules. `install.py` and `uninstall.py` at the repository root were replaced
by `claude-cache-timer install` and `claude-cache-timer uninstall`.

### Removed

- Status line wrapping. Earlier versions kept an existing status line and appended the
countdown to its output, which meant running the wrapped command under `/bin/sh` or
`cmd.exe`, often not the shell it was written for. The installer now stops when it finds
a status line it did not write, and `--force` replaces it.

## [1.0.0] - 2026-08-07

### Added

- Initial release. A status line segment counting down the session's prompt cache, reading
both the remaining time and the cache TTL out of the session transcript, with no hooks
and no state file.

[Unreleased]: https://github.com/GabrielAndreiPreda/claude-code-cache-timer/compare/v2.1.0...HEAD
[2.1.0]: https://github.com/GabrielAndreiPreda/claude-code-cache-timer/compare/v2.0.0...v2.1.0
[2.0.0]: https://github.com/GabrielAndreiPreda/claude-code-cache-timer/compare/v1.0.0...v2.0.0
[1.0.0]: https://github.com/GabrielAndreiPreda/claude-code-cache-timer/releases/tag/v1.0.0
Loading