Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ab52b46
Expect a filtered pass to leave an idle project out of a fresh output
cboos Nov 8, 2026
5c40f34
Show only the date window in a date-filtered run's pages
cboos Nov 8, 2026
01d81d5
Write a date window's view only where none of its pages is, or --force
cboos Nov 8, 2026
7a64975
Document the date window's view, its destination and --force
cboos Nov 8, 2026
c24c227
Pin that only the view's own pages make a destination refuse it
cboos Nov 8, 2026
ed6fd4c
Count the window's session pages in a filtered pass's progress line
cboos Nov 8, 2026
34dbe70
Give the end-to-end date filter test an output path that does not exist
cboos Nov 8, 2026
365880f
Mark the session pages a windowed -o export writes into the project
cboos Nov 8, 2026
baa0fa0
Say which pages of a date-window view state the window
cboos Nov 8, 2026
d5cc87d
Give the one-project date-window examples commands that work
cboos Nov 8, 2026
c780dbf
List the window's sessions on a date-window view's recent page
cboos Nov 8, 2026
38e22a8
Say on a date-window view's search page that it searches everything
cboos Nov 8, 2026
c9c19cc
Offer the combined page alone when one project's window view is refused
cboos Nov 8, 2026
8fc3af1
Say what a date-window view's recent and search pages show
cboos Nov 8, 2026
77f53f6
Classify the recent page's window query in the connection census
cboos Nov 8, 2026
662ef63
Pin which refusals keep "another --output", and the recent page's header
cboos Nov 8, 2026
43511e5
Offer only --force when one project's session pages alone are refused
cboos Nov 8, 2026
58ab9e4
Show the window's sessions in a date-filtered run without a cache
cboos Nov 8, 2026
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
21 changes: 14 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -660,13 +660,20 @@ The tool implements a SQLite-based caching system for performance:
its total at 0 until it finishes. Only the refresh writes entry rows: a run
renders what its refresh committed, so lines written to a session while it
renders appear on the next run. A date-filtered run refreshes the
cache as an unfiltered one would and leaves the files it wrote from its
window marked stale, so the next unfiltered run re-renders them. Its
refresh therefore loads whole files when they changed and the incremental
refresh declines, not just the window's entries, and nothing when no file
changed. A date-filtered `--all-projects` pass converts the projects with a
session in the window (or a changed file, or no combined page at the
destination) and reports the others as having no messages in it. A pass that loses a project ends with a count of the
cache as an unfiltered one would (whole files when they changed and the
incremental refresh declines, nothing when no file changed); an
`--all-projects` pass refreshes every project before converting any,
serially in the parent, to read which sessions the window shows. It
writes a view of the window (`window_session_ids` decides its session
pages, the index's links and the check below alike), into a destination
that holds none of its pages or, with `--force`, over them, removing
nothing; the vendored `assets/` and referenced images are not checked.
Written in place, it leaves the `html_cache`/`html_pages` rows of the
files it replaced marked stale, so the next unfiltered run re-renders
them; written anywhere else it touches no such row, since they are keyed
by project and file name and describe the full view. The master
`index.html`, `search.html` and the recent page have no row: every run
rewrites them. A pass that loses a project ends with a count of the
failed projects, and the CLI exits 1
- **Performance**: 10-100x faster loading for large projects

Expand Down
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ claude-code-log --all-projects
# Process all projects and open in browser
claude-code-log --open-browser

# Process all projects with date filtering
claude-code-log --from-date "yesterday" --to-date "today"
claude-code-log --from-date "last week"
# A view of a date window, in a directory of its own
claude-code-log --from-date "yesterday" --to-date "today" -o ~/yesterday
claude-code-log --from-date "last week" -o ~/last-week

# Skip individual session files (only create combined transcripts)
claude-code-log --no-individual-sessions
Expand Down Expand Up @@ -152,10 +152,19 @@ claude-code-log /path/to/directory -o combined_transcripts.html
claude-code-log /path/to/directory --open-browser

# Filter by date range (supports natural language)
claude-code-log /path/to/directory --from-date "yesterday" --to-date "today"
claude-code-log /path/to/directory --from-date "3 days ago" --to-date "yesterday"
claude-code-log /path/to/directory --from-date "yesterday" --to-date "today" --combined only -o ~/yesterday.html
claude-code-log /path/to/directory --from-date "3 days ago" --to-date "yesterday" --force
```

A date-filtered run writes a view of its window: only the projects and
sessions with messages in it, with the window's totals. It goes into a
destination that holds none of the pages it writes, so over the pages of
an earlier run it refuses to start: give it another `-o`, or `--force` to
write over them (nothing is removed). For one directory, `-o` names the
combined page alone and the session pages go into the directory, hence
`--combined only` above. See [Viewing a date
window](https://daaain.github.io/claude-code-log/date-window/).

### Feeding Past Conversations to an LLM

The combination `--depth agent --format md --compact` produces condensed Markdown suitable as context for an LLM to review or distill patterns from past work:
Expand Down
41 changes: 1 addition & 40 deletions claude_code_log/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import threading
import zlib
from contextlib import contextmanager
from datetime import datetime, timezone
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, get_args

Expand Down Expand Up @@ -3451,45 +3451,6 @@ def update_html_cache(
),
)

def has_sessions_in_window(
self, from_dt: Optional[datetime], to_dt: Optional[datetime]
) -> bool:
"""Does a visible session of this project overlap the window?

Read from the session rows' first and last timestamps, so it is
only as current as those rows: a caller must not trust it for a
project whose files changed since. ``from_dt`` / ``to_dt`` are
naive UTC, as ``utils.parse_date_bounds`` returns them. A session
whose stamps do not parse counts as overlapping.
"""
from .utils import parse_timestamp

if self._project_id is None:
return True

def naive_utc(stamp: Optional[str]) -> Optional[datetime]:
parsed = parse_timestamp(stamp)
if parsed is None:
return None
return parsed.astimezone(timezone.utc).replace(tzinfo=None)

with self._get_connection() as conn:
rows = conn.execute(
"""SELECT first_timestamp, last_timestamp FROM sessions
WHERE project_id = ? AND hidden = 0""",
(self._project_id,),
).fetchall()
for row in rows:
first = naive_utc(row["first_timestamp"])
last = naive_utc(row["last_timestamp"])
if first is None or last is None:
return True
if (to_dt is None or first <= to_dt) and (
from_dt is None or last >= from_dt
):
return True
return False

def mark_rendered_from_window(self, html_path: str) -> None:
"""Record that ``html_path`` was just written from a date window.

Expand Down
30 changes: 29 additions & 1 deletion claude_code_log/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from .converter import (
RegenerationReport,
WindowOutputExists,
convert_jsonl_to,
convert_jsonl_to_html,
ensure_fresh_cache,
Expand Down Expand Up @@ -1015,6 +1016,16 @@ def main() -> None:
type=str,
help='Filter messages up to this date/time (e.g., "1 hour ago", "today", "2025-06-08 15:00")',
)
@click.option(
"--force",
is_flag=True,
help=(
"With --from-date/--to-date: write the date window's view over the "
"files of it that already exist at the destination. Without it, such "
"a run refuses to start when any does. Nothing is removed: pages of "
"an earlier view that this one does not rewrite stay, unlinked."
),
)
@click.option(
"--all-projects",
is_flag=True,
Expand Down Expand Up @@ -1214,6 +1225,7 @@ def convert(
open_browser: bool,
from_date: Optional[str],
to_date: Optional[str],
force: bool,
all_projects: bool,
no_individual_sessions: bool,
no_cache: bool,
Expand Down Expand Up @@ -1260,6 +1272,11 @@ def convert(
# usage error here, before any work starts.
resolved_theme = _resolve_theme(theme)

# `--force` lets a date window's view overwrite the files of it already
# at its destination; there is nothing else for it to force yet.
if force and from_date is None and to_date is None:
raise click.UsageError("--force is only accepted with --from-date/--to-date.")

# Provider mode has three sub-modes:
# * export — `--session-id <id>`: render one session by id
# * single-file — INPUT_PATH is a rollout FILE: render that one session
Expand Down Expand Up @@ -1299,6 +1316,10 @@ def convert(
conflicts: list[str] = []
if tui:
conflicts.append("--tui")
if force:
# The provider pipeline writes its date window over a full view
# as before, so there is nothing for --force to allow.
conflicts.append("--force")
if provider_wholesale:
# Wholesale honors --expand-paths/--filter-path (Obsidian projection),
# --combined, date range, -o/-f, --open-browser, and the cache flags
Expand Down Expand Up @@ -1940,6 +1961,7 @@ def render_provider(destination: Path) -> Path:
theme=resolved_theme,
jobs=jobs,
failed_projects=failed_projects,
force=force,
)

# Count processed projects
Expand Down Expand Up @@ -1979,6 +2001,11 @@ def render_provider(destination: Path) -> Path:
from .providers import discover_providers

detected = discover_providers().detect_provider_for_path(input_path)
if detected is not None and force:
raise click.UsageError(
f"--force does not apply to {input_path}, a {detected} "
"input: its date-filtered view is written as before."
)
if detected is not None:
if input_path.is_dir():
_run_provider_wholesale(
Expand Down Expand Up @@ -2122,6 +2149,7 @@ def render_provider(destination: Path) -> Path:
render_jobs=(
min(jobs, resolve_render_jobs(None)) if jobs is not None else None
),
force=force,
)
# Report only work actually done this run. On a pure skip the converter
# already printed its "... is current, skipping regeneration" line, so
Expand Down Expand Up @@ -2160,7 +2188,7 @@ def render_provider(destination: Path) -> Path:
if open_browser:
click.launch(str(output_path))

except FileNotFoundError as e:
except (FileNotFoundError, WindowOutputExists) as e:
click.echo(f"Error: {e}", err=True)
if debug:
import traceback
Expand Down
Loading
Loading