claude-code-log 1.4.0__tar.gz → 1.5.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.
- claude_code_log-1.5.0/.devbox-foldyard/.gitignore +1 -0
- claude_code_log-1.5.0/.devbox-foldyard/foldyard-0.0.1-py3-none-any.whl +0 -0
- claude_code_log-1.5.0/.github/workflows/ci.yml +117 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/CHANGELOG.md +60 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/PKG-INFO +1 -1
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/cache.py +273 -37
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/cli.py +243 -13
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/converter.py +611 -173
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/dag.py +114 -15
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/meta_factory.py +8 -1
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/system_factory.py +3 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/tool_factory.py +75 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/__init__.py +4 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/async_formatter.py +2 -2
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/renderer.py +225 -33
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/system_formatters.py +16 -13
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/global_styles.css +15 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/message_styles.css +521 -11
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/session_nav.html +8 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/session_nav_styles.css +8 -3
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/timeline.html +28 -6
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/transcript.html +310 -297
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/tool_formatters.py +669 -46
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/user_formatters.py +154 -1
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/utils.py +268 -25
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/json/renderer.py +12 -3
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/markdown/renderer.py +372 -25
- claude_code_log-1.5.0/claude_code_log/migrations/007_subagents_fingerprint.sql +16 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/models.py +157 -7
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/plugins.py +47 -9
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/renderer.py +1073 -275
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/utils.py +92 -3
- claude_code_log-1.5.0/claude_code_log/workflow.py +661 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/agents.md +182 -4
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/application_model.md +64 -20
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/css-classes.md +4 -1
- claude_code_log-1.5.0/dev-docs/dag.md +721 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/implementing-a-tool-renderer.md +24 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/message-hierarchy.md +40 -36
- claude_code_log-1.5.0/dev-docs/messages/tools/Artifact-tool_result.json +24 -0
- claude_code_log-1.5.0/dev-docs/messages/tools/Artifact-tool_result.jsonl +1 -0
- claude_code_log-1.5.0/dev-docs/messages/tools/Artifact-tool_use.json +27 -0
- claude_code_log-1.5.0/dev-docs/messages/tools/Artifact-tool_use.jsonl +1 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages.md +38 -1
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/plugins.md +89 -12
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/rendering-architecture.md +17 -8
- claude_code_log-1.5.0/dev-docs/workflows.md +341 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/justfile +6 -3
- claude_code_log-1.5.0/mise.toml +8 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/mkdocs.yml +8 -1
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/pyproject.toml +19 -2
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/uv.lock +23 -23
- claude_code_log-1.5.0/work/agent-hierarchies-design.md +229 -0
- claude_code_log-1.5.0/work/parse-memory-spike.md +140 -0
- claude_code_log-1.5.0/work/tui-output-fixes.md +75 -0
- claude_code_log-1.5.0/work/win-io.md +79 -0
- claude_code_log-1.5.0/work/xdist-import-cost.md +133 -0
- claude_code_log-1.4.0/.github/workflows/ci.yml +0 -87
- claude_code_log-1.4.0/dev-docs/dag.md +0 -701
- claude_code_log-1.4.0/mise.toml +0 -2
- claude_code_log-1.4.0/work/refactor-reindex-with-ghosting.md +0 -197
- claude_code_log-1.4.0/work/simplify-converter-renderer.md +0 -312
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.claude/settings.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.claude/skills/tool-renderer/SKILL.md +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.github/workflows/docs.yml +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.gitignore +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/.vscode/settings.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/CLAUDE.md +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/CONTRIBUTING.md +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/LICENSE +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/README.md +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/__init__.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/__init__.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/agent_metadata_factory.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/assistant_factory.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/attachment_factory.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/priorities.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/task_notification_factory.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/teammate_factory.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/transcript_factory.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/factories/user_factory.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/git_remote.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/ansi_colors.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/assistant_formatters.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/renderer_code.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/teammate_formatter.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/edit_diff_styles.css +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/filter_styles.css +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/page_nav_styles.css +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/project_card_styles.css +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/pygments_styles.css +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search.html +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search_inline.html +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search_inline_script.html +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search_results_panel.html +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/search_styles.css +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/teammate_styles.css +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/timeline_styles.css +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/timezone_converter.js +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/components/todo_styles.css +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/html/templates/index.html +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/image_export.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/json/__init__.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/markdown/__init__.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/markdown_plugins.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/001_initial_schema.sql +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/002_html_cache.sql +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/003_html_pagination.sql +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/004_html_pagination_variant.sql +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/005_session_team_name.sql +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/006_session_ai_title.sql +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/__init__.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/migrations/runner.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/parser.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/py.typed +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/renderer_timings.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/claude_code_log/tui.py +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/assistant.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/assistant.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/assistant_sidechain.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/assistant_sidechain.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/thinking.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/assistant/thinking.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/file_history_snapshot.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/file_history_snapshot.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/queue_operation.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/queue_operation.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/summary.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/summary.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/system_info.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/system/system_info.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_result_error.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_result_error.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/AskUserQuestion-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_result_error.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_result_error.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Bash-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/BashOutput-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/BashOutput-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/BashOutput-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/BashOutput-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_result_error.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_result_error.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Edit-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_result_error.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_result_error.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/ExitPlanMode-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Glob-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Glob-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Glob-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Glob-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Grep-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Grep-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Grep-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Grep-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_result_error.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_result_error.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/KillShell-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/LS-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/LS-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/LS-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/LS-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_result_error.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_result_error.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/MultiEdit-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_result_error.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_result_error.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Read-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Task-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Task-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Task-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Task-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/TodoWrite-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/TodoWrite-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/TodoWrite-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/TodoWrite-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebFetch-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebFetch-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebFetch-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebFetch-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebSearch-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebSearch-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebSearch-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/WebSearch-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_result_error.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_result_error.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/Write-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/exit_plan_mode-tool_result.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/exit_plan_mode-tool_result.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/exit_plan_mode-tool_use.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/tools/exit_plan_mode-tool_use.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/bash_input.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/bash_input.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/bash_output.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/bash_output.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/command_output.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/command_output.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/image.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/image.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_command.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_command.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_sidechain.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_sidechain.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_slash_command.json +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/messages/user/user_slash_command.jsonl +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/dev-docs/teammates.md +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/__init__.pyi +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/formatter.pyi +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/formatters/__init__.pyi +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/lexer.pyi +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/lexers/__init__.pyi +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/stubs/pygments/util.pyi +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/work/obsidian-friendly-output.md +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/work/rendering-next.md +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/work/session-state-propagation.md +0 -0
- {claude_code_log-1.4.0 → claude_code_log-1.5.0}/work/tool-renderer-plugins.md +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
Binary file
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
os: ["ubuntu-latest", "windows-latest"]
|
|
15
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
16
|
+
include:
|
|
17
|
+
- os: "ubuntu-latest"
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
is-primary: true
|
|
20
|
+
|
|
21
|
+
env:
|
|
22
|
+
# Collect coverage only on the primary job: it is the only one that
|
|
23
|
+
# uploads the reports and feeds the PR comment, while --cov costs
|
|
24
|
+
# ~65% extra pytest wall time (worst on Windows). Windows-only
|
|
25
|
+
# covered lines measured at 5 (diffing covered lines per file,
|
|
26
|
+
# Windows vs the primary artifact): the SIGUSR1 no-op return and the
|
|
27
|
+
# drive-letter branch of convert_project_path_to_claude_dir, both in
|
|
28
|
+
# cli.py. Those were never in the published report (non-primary
|
|
29
|
+
# coverage was computed and discarded), so this changes nothing.
|
|
30
|
+
COV: ${{ matrix.is-primary && '--cov=claude_code_log --cov-report=xml --cov-report=html --cov-report=term' || '' }}
|
|
31
|
+
COV_APPEND: ${{ matrix.is-primary && '--cov=claude_code_log --cov-append --cov-report=xml --cov-report=html --cov-report=term' || '' }}
|
|
32
|
+
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
with:
|
|
36
|
+
# Full history so test/test_commit_linkifier.py's
|
|
37
|
+
# TestIntegrationLocalRepo can resolve SHAs older than the
|
|
38
|
+
# current tip (`git branch -r --contains` reads local refs
|
|
39
|
+
# only and needs the commits in the object DB).
|
|
40
|
+
fetch-depth: 0
|
|
41
|
+
|
|
42
|
+
- name: Move TEMP to the workspace disk (Windows)
|
|
43
|
+
# On windows runners TMP/TEMP default to the C: OS disk, whose
|
|
44
|
+
# throttled I/O dominates this tmp-heavy suite; RUNNER_TEMP lives on
|
|
45
|
+
# the same disk as the workspace (D:). Measured via A/B experiment
|
|
46
|
+
# (run 28715118956): this redirect alone took the unit selection from
|
|
47
|
+
# 555s mean to 150s — parity with ubuntu (~161s). Defender is NOT the
|
|
48
|
+
# culprit: real-time protection is already off on current images
|
|
49
|
+
# (exclusions/disable measured as no-ops), and a ReFS dev drive
|
|
50
|
+
# (~296s) was dominated by this redirect. Details: work/win-io.md
|
|
51
|
+
if: runner.os == 'Windows'
|
|
52
|
+
shell: pwsh
|
|
53
|
+
run: |
|
|
54
|
+
"TMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
|
|
55
|
+
"TEMP=$env:RUNNER_TEMP" >> $env:GITHUB_ENV
|
|
56
|
+
|
|
57
|
+
- name: Install uv
|
|
58
|
+
uses: astral-sh/setup-uv@v4
|
|
59
|
+
with:
|
|
60
|
+
enable-cache: true
|
|
61
|
+
|
|
62
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
63
|
+
run: uv python install ${{ matrix.python-version }}
|
|
64
|
+
|
|
65
|
+
- name: Install dependencies
|
|
66
|
+
run: uv sync --all-extras --dev && uv run playwright install chromium
|
|
67
|
+
|
|
68
|
+
- name: Run unit tests (coverage on primary only)
|
|
69
|
+
# -p no:playwright: unit tests don't use the browser fixtures; skipping the
|
|
70
|
+
# plugin avoids a ~1s playwright import per xdist worker, which Windows pays
|
|
71
|
+
# per spawned worker. See work/xdist-import-cost.md.
|
|
72
|
+
run: uv run pytest -p no:playwright -m "not (tui or browser or benchmark)" ${{ env.COV }}
|
|
73
|
+
|
|
74
|
+
- name: Run TUI tests (coverage append on primary only)
|
|
75
|
+
run: uv run pytest -m tui ${{ env.COV_APPEND }}
|
|
76
|
+
|
|
77
|
+
- name: Run browser tests (coverage append on primary only)
|
|
78
|
+
run: uv run pytest -m browser ${{ env.COV_APPEND }}
|
|
79
|
+
|
|
80
|
+
- name: Run benchmark tests with coverage append (primary only)
|
|
81
|
+
if: matrix.is-primary
|
|
82
|
+
env:
|
|
83
|
+
CLAUDE_CODE_LOG_DEBUG_TIMING: "1"
|
|
84
|
+
run: uv run pytest -m benchmark ${{ env.COV_APPEND }} -v
|
|
85
|
+
|
|
86
|
+
- name: Upload coverage HTML report as artifact
|
|
87
|
+
uses: actions/upload-artifact@v4
|
|
88
|
+
if: matrix.is-primary
|
|
89
|
+
with:
|
|
90
|
+
name: coverage-report-html
|
|
91
|
+
path: htmlcov/
|
|
92
|
+
retention-days: 30
|
|
93
|
+
|
|
94
|
+
- name: Upload coverage XML as artifact
|
|
95
|
+
uses: actions/upload-artifact@v4
|
|
96
|
+
if: matrix.is-primary
|
|
97
|
+
with:
|
|
98
|
+
name: coverage-report-xml
|
|
99
|
+
path: coverage.xml
|
|
100
|
+
retention-days: 30
|
|
101
|
+
|
|
102
|
+
- name: Coverage comment (PR only)
|
|
103
|
+
if: github.event_name == 'pull_request' && matrix.is-primary
|
|
104
|
+
uses: py-cov-action/python-coverage-comment-action@v3
|
|
105
|
+
with:
|
|
106
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
107
|
+
MINIMUM_GREEN: 75
|
|
108
|
+
MINIMUM_ORANGE: 60
|
|
109
|
+
|
|
110
|
+
- name: Run linting
|
|
111
|
+
run: uv run ruff check
|
|
112
|
+
|
|
113
|
+
- name: Run formatting check
|
|
114
|
+
run: uv run ruff format --check
|
|
115
|
+
|
|
116
|
+
- name: Run type checking
|
|
117
|
+
run: uv run pyright
|
|
@@ -6,6 +6,66 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
## [1.5.0] - 2026-07-09
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Filter <local-command-caveat>-wrapped caveat messages (Claude Code ≥ ~2.1) (#269)**
|
|
14
|
+
- **Fix perpetual re-regeneration of projects with up-to-date caches (#268)**
|
|
15
|
+
- **Fix ty possibly-missing-attribute warnings in tests (#267)**
|
|
16
|
+
- **Speed up cache freshness checks on large archives (#266)**
|
|
17
|
+
- **ci: move TMP/TEMP to the workspace disk on Windows runners**
|
|
18
|
+
- **perf(cov): use sys.monitoring coverage core (COVERAGE_CORE=sysmon equivalent)**
|
|
19
|
+
- **ci: collect coverage only on the primary matrix job**
|
|
20
|
+
- **Improve Artifact tool look&feel: label-linked result, lean tool use (#262) (#263)**
|
|
21
|
+
- **Remap dangling parent refs when dedup drops a distinct DAG node (#259) (#261)**
|
|
22
|
+
- **Render the Artifact tool (deploy HTML/MD page → claude.ai URL) (#257) (#260)**
|
|
23
|
+
- **Handle the full variety of Workflow invocation shapes (#174 follow-up) (#258)**
|
|
24
|
+
- **Regenerate a --no-cache directory when a source .jsonl grows (#254) (#256)**
|
|
25
|
+
- **Regenerate single-file output when its source grows; fix success-message accuracy (#221 follow-up) (#253)**
|
|
26
|
+
- **Surface the model each agent ran on, once per agent context (#246) (#252)**
|
|
27
|
+
- **feat: use nicer glyphs for fold/unfold controls (#247) (#255)**
|
|
28
|
+
- **Escape raw HTML in assistant/tool content to fix XSS (#245)**
|
|
29
|
+
- **Warn that --output / --format are ignored under --tui (#220) (#241)**
|
|
30
|
+
- **Speed up the SQLite metadata-cache build (connection reuse + synchronous=NORMAL) (#251)**
|
|
31
|
+
- **Cut pytest-xdist per-worker import cost on Windows unit runs (#250)**
|
|
32
|
+
- **Speed up integration-test fixtures on Windows (shrink copied transcripts; skip symlink tests when unavailable) (#249)**
|
|
33
|
+
- **Stream output to stdout: support -o - / /dev/stdout (#223 part 2) (#239)**
|
|
34
|
+
- **Keep fork points visible when their host message is detail-filtered (#233 follow-up) (#240)**
|
|
35
|
+
- **Explicit --output: always regenerate, infer format from suffix, fix stdout hang (#221, #222, #223 part 1) (#237)**
|
|
36
|
+
- **Connect fork-point and branch headers to their anchor message (#238)**
|
|
37
|
+
- **Anchor fork points whose node is an unrendered system message (#236)**
|
|
38
|
+
- **Render Read/Write of any Markdown file as Markdown, not Pygments (#232) (#234)**
|
|
39
|
+
- **Generic tool results: render structured content items (#227) (#235)**
|
|
40
|
+
- **Nested agent hierarchies: visual layer (#213, part 2 — WIP) (#219)**
|
|
41
|
+
- **Render nested sub-agent hierarchies (#213) (#218)**
|
|
42
|
+
- **Build(deps): bump pygments from 2.19.2 to 2.20.0 (#228)**
|
|
43
|
+
- **Build(deps): bump requests from 2.32.5 to 2.33.0 (#229)**
|
|
44
|
+
- **Build(deps-dev): bump pytest from 9.0.1 to 9.0.3 (#230)**
|
|
45
|
+
- **Build(deps): bump idna from 3.11 to 3.15 (#231)**
|
|
46
|
+
- **Side-channel prompt rendering + params-fold UX and accessibility (#174 follow-up) (#217)**
|
|
47
|
+
- **Polish the workflow run tree + dev-docs sync (#174 follow-up) (#215)**
|
|
48
|
+
- **Hybrid JSON/Markdown rendering for tool params tables (#216)**
|
|
49
|
+
- **Linearize assistant-continuation tool-flow forks instead of branching (#214)**
|
|
50
|
+
- **Enrich fork-point and branch nav labels with message summaries (#179 follow-up) (#212)**
|
|
51
|
+
- **Render dynamic-workflow run tree on the nested DOM (#174 PR3) (#210)**
|
|
52
|
+
- **Keep recaps visible at all detail levels; add --no-recaps (#179) (#209)**
|
|
53
|
+
- **Dedupe "unrecognized message type" warnings per file (#211)**
|
|
54
|
+
- **work/: delete the completed converter/renderer simplification plans (#208)**
|
|
55
|
+
- **Flag auto-memory interactions: 🧠 titles + memory filter (#192) (#204)**
|
|
56
|
+
- **Phase 3: single-axis collapse — delete pre-render _filter_by_detail (#206)**
|
|
57
|
+
- **Render Workflow tool-input (JS) + async-result JSON bodies (#174 PR2) (#205)**
|
|
58
|
+
- **Phase 2 of ghosting epic: detail-filter ghosts in place; drop _reindex_filtered_context (#194)**
|
|
59
|
+
- **Parse dynamic-Workflow runs into WorkflowRun models (#174 PR1) (#203)**
|
|
60
|
+
- **Bump mistune from 3.1.4 to 3.2.1 (#202)**
|
|
61
|
+
- **Bump gitpython from 3.1.45 to 3.1.50 (#200)**
|
|
62
|
+
- **Exclude docs/ from ty type-checking (#201)**
|
|
63
|
+
- **Render message tree as nested DOM (PR0 for #174) (#191)**
|
|
64
|
+
- **Bump urllib3 from 2.5.0 to 2.7.0 (#199)**
|
|
65
|
+
- **Ghost-aware ctx.messages; skill-fold uses None slots (ghosting epic Phase 1) (#193)**
|
|
66
|
+
- **Merge pull request #198 from daaain/fix-linux-release**
|
|
67
|
+
|
|
68
|
+
|
|
9
69
|
## [1.4.0] - 2026-06-03
|
|
10
70
|
|
|
11
71
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-code-log
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Convert Claude Code transcript JSONL files to HTML
|
|
5
5
|
Project-URL: Homepage, https://github.com/daaain/claude-code-log
|
|
6
6
|
Project-URL: Issues, https://github.com/daaain/claude-code-log/issues
|
|
@@ -10,7 +10,7 @@ import zlib
|
|
|
10
10
|
from contextlib import contextmanager
|
|
11
11
|
from datetime import datetime, timezone
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import Any, Dict, Generator, List, Optional
|
|
13
|
+
from typing import Any, Callable, Dict, Generator, List, Optional
|
|
14
14
|
|
|
15
15
|
from packaging import version
|
|
16
16
|
from pydantic import BaseModel
|
|
@@ -39,7 +39,6 @@ class CachedFileInfo(BaseModel):
|
|
|
39
39
|
source_mtime: float
|
|
40
40
|
cached_mtime: float
|
|
41
41
|
message_count: int
|
|
42
|
-
session_ids: list[str]
|
|
43
42
|
|
|
44
43
|
|
|
45
44
|
class SessionCacheData(BaseModel):
|
|
@@ -220,6 +219,72 @@ def get_library_version() -> str:
|
|
|
220
219
|
# ========== Cache Path Configuration ==========
|
|
221
220
|
|
|
222
221
|
|
|
222
|
+
def subagents_fingerprint(jsonl_path: Path) -> str:
|
|
223
|
+
"""Fingerprint of the sidecar inputs that feed spawn discovery (#213).
|
|
224
|
+
|
|
225
|
+
Parsing a transcript also reads ``agent-*.meta.json`` sidecars (see
|
|
226
|
+
``converter._subagent_meta_map``), so they must take part in cache
|
|
227
|
+
invalidation: a sidecar appearing after the transcript was cached
|
|
228
|
+
(e.g. a still-running agent spawning a child without touching the
|
|
229
|
+
trunk file) would otherwise go unnoticed by the mtime-only check.
|
|
230
|
+
|
|
231
|
+
The fingerprint is ``"<count>:<newest int mtime>"`` over the sidecars
|
|
232
|
+
of the dirs where this transcript's children can live — the sibling
|
|
233
|
+
``<stem>/subagents/`` dir, plus the containing dir for agent files
|
|
234
|
+
(the flat layout puts children next to their parent). Sidecars are
|
|
235
|
+
written once at spawn time, so count+newest-mtime captures every
|
|
236
|
+
addition and removal. Empty string when there are none.
|
|
237
|
+
|
|
238
|
+
Deliberately narrower than ``converter._subagent_meta_map``'s scan
|
|
239
|
+
for TRUNK files: the parse also defensively checks the project dir
|
|
240
|
+
itself, but Claude Code never writes sidecars there, and
|
|
241
|
+
fingerprinting it would rescan a potentially large dir on every
|
|
242
|
+
cache read for a theoretical-only input.
|
|
243
|
+
"""
|
|
244
|
+
candidates = [jsonl_path.parent / jsonl_path.stem / "subagents"]
|
|
245
|
+
if jsonl_path.parent.name == "subagents":
|
|
246
|
+
candidates.append(jsonl_path.parent)
|
|
247
|
+
metas: list[Path] = []
|
|
248
|
+
for directory in candidates:
|
|
249
|
+
if directory.is_dir():
|
|
250
|
+
metas.extend(directory.glob("agent-*.meta.json"))
|
|
251
|
+
if not metas:
|
|
252
|
+
return ""
|
|
253
|
+
try:
|
|
254
|
+
newest = max(int(p.stat().st_mtime) for p in metas)
|
|
255
|
+
except OSError:
|
|
256
|
+
# A sidecar vanished mid-scan; treat as unstable so the next
|
|
257
|
+
# check re-reads (an always-mismatching fingerprint is safe).
|
|
258
|
+
return f"{len(metas)}:unstable"
|
|
259
|
+
return f"{len(metas)}:{newest}"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _cache_row_is_fresh(
|
|
263
|
+
row: sqlite3.Row, source_mtime: float, current_fp: Callable[[], str]
|
|
264
|
+
) -> bool:
|
|
265
|
+
"""Decide whether a cached_files row is still fresh.
|
|
266
|
+
|
|
267
|
+
Single source of truth for the freshness rule shared by
|
|
268
|
+
is_file_cached() and get_modified_files(). current_fp is a callable
|
|
269
|
+
so callers only pay for the sidecar fingerprint scan when the mtime
|
|
270
|
+
check passes (and get_modified_files() can plug in its
|
|
271
|
+
scandir-optimized variant).
|
|
272
|
+
|
|
273
|
+
Cache is valid if modification times match (within 1 second
|
|
274
|
+
tolerance) and the sidecar inputs of spawn discovery (#213) match
|
|
275
|
+
too — new agent-*.meta.json files appear without touching the
|
|
276
|
+
source jsonl. Pre-007 rows carry NULL: accept those only when the
|
|
277
|
+
file has no sidecars today (nothing to miss), so legacy caches
|
|
278
|
+
don't mass-invalidate while sessions WITH sidecars reparse once.
|
|
279
|
+
"""
|
|
280
|
+
if abs(source_mtime - row["source_mtime"]) >= 1.0:
|
|
281
|
+
return False
|
|
282
|
+
cached_fp = row["subagents_fingerprint"]
|
|
283
|
+
if cached_fp is None:
|
|
284
|
+
return current_fp() == ""
|
|
285
|
+
return cached_fp == current_fp()
|
|
286
|
+
|
|
287
|
+
|
|
223
288
|
def get_cache_db_path(projects_dir: Path) -> Path:
|
|
224
289
|
"""Get cache database path, respecting CLAUDE_CODE_LOG_CACHE_PATH env var.
|
|
225
290
|
|
|
@@ -240,6 +305,16 @@ def get_cache_db_path(projects_dir: Path) -> Path:
|
|
|
240
305
|
# ========== Cache Manager ==========
|
|
241
306
|
|
|
242
307
|
|
|
308
|
+
# Process-level memo of cache DBs whose migrations have already been
|
|
309
|
+
# checked. CacheManager is constructed per project (the TUI project
|
|
310
|
+
# selector builds one per table row, on every resize), so without the
|
|
311
|
+
# memo every instance re-pays the migration check: a connection plus
|
|
312
|
+
# version-table and migration-directory reads, ~2ms each (issue #12).
|
|
313
|
+
# The exists() guard in _init_database covers --clear-cache deleting
|
|
314
|
+
# the DB file mid-process.
|
|
315
|
+
_migrated_db_paths: set[str] = set()
|
|
316
|
+
|
|
317
|
+
|
|
243
318
|
class CacheManager:
|
|
244
319
|
"""SQLite-based cache manager for Claude Code Log."""
|
|
245
320
|
|
|
@@ -266,27 +341,109 @@ class CacheManager:
|
|
|
266
341
|
else:
|
|
267
342
|
self.db_path = get_cache_db_path(project_path.parent)
|
|
268
343
|
|
|
344
|
+
# When inside a batch() scope this holds the single shared connection
|
|
345
|
+
# reused by every _get_connection() call; None means the default
|
|
346
|
+
# open-a-connection-per-call behaviour. Set before _init_database() so
|
|
347
|
+
# the field always exists before any _get_connection() could run, even
|
|
348
|
+
# if migrations are ever routed through it.
|
|
349
|
+
self._shared_conn: Optional[sqlite3.Connection] = None
|
|
350
|
+
|
|
269
351
|
# Initialise database and ensure project exists
|
|
270
352
|
self._init_database()
|
|
271
353
|
self._project_id: Optional[int] = None
|
|
272
354
|
self._ensure_project_exists()
|
|
273
355
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"""Get a database connection with proper settings."""
|
|
277
|
-
conn = sqlite3.connect(self.db_path, timeout=30.0)
|
|
356
|
+
def _configure_connection(self, conn: sqlite3.Connection) -> None:
|
|
357
|
+
"""Apply the standard pragmas/row factory to a fresh connection."""
|
|
278
358
|
conn.row_factory = sqlite3.Row
|
|
279
359
|
conn.execute("PRAGMA foreign_keys = ON")
|
|
280
360
|
conn.execute("PRAGMA journal_mode = WAL")
|
|
361
|
+
# synchronous=NORMAL is the recommended pairing for WAL: it keeps
|
|
362
|
+
# durability across application crashes (only a power/OS crash can lose
|
|
363
|
+
# the last committed transaction) while skipping an fsync on every
|
|
364
|
+
# commit. The cache is fully regenerable from the JSONL source, so that
|
|
365
|
+
# residual risk is acceptable.
|
|
366
|
+
conn.execute("PRAGMA synchronous = NORMAL")
|
|
367
|
+
|
|
368
|
+
def _open_configured_connection(self) -> sqlite3.Connection:
|
|
369
|
+
"""Open a connection and apply pragmas, closing it if setup fails.
|
|
370
|
+
|
|
371
|
+
If a PRAGMA in ``_configure_connection`` raises, the just-opened
|
|
372
|
+
handle is closed before re-raising so it can't leak and lock the
|
|
373
|
+
.db/.db-wal/.db-shm files — the exact failure mode the connection
|
|
374
|
+
lifecycle elsewhere is careful to avoid (Windows WinError 32).
|
|
375
|
+
"""
|
|
376
|
+
conn = sqlite3.connect(self.db_path, timeout=30.0)
|
|
377
|
+
try:
|
|
378
|
+
self._configure_connection(conn)
|
|
379
|
+
except BaseException:
|
|
380
|
+
conn.close()
|
|
381
|
+
raise
|
|
382
|
+
return conn
|
|
383
|
+
|
|
384
|
+
@contextmanager
|
|
385
|
+
def _get_connection(self) -> Generator[sqlite3.Connection, None, None]:
|
|
386
|
+
"""Get a database connection with proper settings.
|
|
387
|
+
|
|
388
|
+
Inside a ``batch()`` scope this yields the shared connection without
|
|
389
|
+
closing it (the batch owns its lifecycle). Otherwise it opens a fresh
|
|
390
|
+
connection and closes it on exit — the default, Windows-safe behaviour
|
|
391
|
+
(no lingering file handle on the .db/.db-wal/.db-shm files).
|
|
392
|
+
"""
|
|
393
|
+
if self._shared_conn is not None:
|
|
394
|
+
yield self._shared_conn
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
conn = self._open_configured_connection()
|
|
281
398
|
try:
|
|
282
399
|
yield conn
|
|
283
400
|
finally:
|
|
284
401
|
conn.close()
|
|
285
402
|
|
|
403
|
+
@contextmanager
|
|
404
|
+
def batch(self) -> Generator[None, None, None]:
|
|
405
|
+
"""Reuse a single connection for every operation within the scope.
|
|
406
|
+
|
|
407
|
+
A full project build issues ~190 ``_get_connection`` calls; opening a
|
|
408
|
+
fresh SQLite connection each time dominates cache-build cost. Wrapping
|
|
409
|
+
the build in ``with cache_manager.batch():`` collapses those to one
|
|
410
|
+
connection.
|
|
411
|
+
|
|
412
|
+
Lifecycle guarantees (the risky part — see the integrity tests):
|
|
413
|
+
- The shared connection is **always closed on scope exit**, including
|
|
414
|
+
when the body raises (the ``finally``). This releases the file lock
|
|
415
|
+
on the .db/.db-wal/.db-shm files *before* any caller tears down a
|
|
416
|
+
TemporaryDirectory or runs ``clear_cache``/rmtree — critical on
|
|
417
|
+
Windows, which refuses to delete open files (WinError 32).
|
|
418
|
+
- Outside a batch, behaviour is unchanged (connection-per-call).
|
|
419
|
+
- Nesting is a no-op: an inner ``batch()`` reuses the outer connection
|
|
420
|
+
and does NOT close it, so the converter loop can't double-open or
|
|
421
|
+
close a connection still in use by an enclosing scope.
|
|
422
|
+
"""
|
|
423
|
+
if self._shared_conn is not None:
|
|
424
|
+
# Already batching — reuse the existing shared connection and leave
|
|
425
|
+
# its lifecycle to the outermost batch().
|
|
426
|
+
yield
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# Open+configure first; only publish to _shared_conn once setup has
|
|
430
|
+
# succeeded, so a failed PRAGMA never leaves a half-initialised (and
|
|
431
|
+
# now-closed) handle assigned for other calls to reuse.
|
|
432
|
+
conn = self._open_configured_connection()
|
|
433
|
+
self._shared_conn = conn
|
|
434
|
+
try:
|
|
435
|
+
yield
|
|
436
|
+
finally:
|
|
437
|
+
self._shared_conn = None
|
|
438
|
+
conn.close()
|
|
439
|
+
|
|
286
440
|
def _init_database(self) -> None:
|
|
287
441
|
"""Create schema if needed using migration runner."""
|
|
288
|
-
|
|
442
|
+
key = str(self.db_path)
|
|
443
|
+
if key in _migrated_db_paths and self.db_path.exists():
|
|
444
|
+
return
|
|
289
445
|
run_migrations(self.db_path)
|
|
446
|
+
_migrated_db_paths.add(key)
|
|
290
447
|
|
|
291
448
|
def _ensure_project_exists(self) -> None:
|
|
292
449
|
"""Ensure project record exists and get its ID."""
|
|
@@ -471,18 +628,19 @@ class CacheManager:
|
|
|
471
628
|
|
|
472
629
|
with self._get_connection() as conn:
|
|
473
630
|
row = conn.execute(
|
|
474
|
-
"SELECT source_mtime FROM cached_files
|
|
631
|
+
"SELECT source_mtime, subagents_fingerprint FROM cached_files"
|
|
632
|
+
" WHERE project_id = ? AND file_name = ?",
|
|
475
633
|
(self._project_id, jsonl_path.name),
|
|
476
634
|
).fetchone()
|
|
477
635
|
|
|
478
636
|
if not row:
|
|
479
637
|
return False
|
|
480
638
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
639
|
+
return _cache_row_is_fresh(
|
|
640
|
+
row,
|
|
641
|
+
jsonl_path.stat().st_mtime,
|
|
642
|
+
lambda: subagents_fingerprint(jsonl_path),
|
|
643
|
+
)
|
|
486
644
|
|
|
487
645
|
def load_cached_entries(self, jsonl_path: Path) -> Optional[List[TranscriptEntry]]:
|
|
488
646
|
"""Load cached transcript entries for a JSONL file."""
|
|
@@ -564,14 +722,27 @@ class CacheManager:
|
|
|
564
722
|
return [self._deserialize_entry(row) for row in rows]
|
|
565
723
|
|
|
566
724
|
def save_cached_entries(
|
|
567
|
-
self,
|
|
725
|
+
self,
|
|
726
|
+
jsonl_path: Path,
|
|
727
|
+
entries: List[TranscriptEntry],
|
|
728
|
+
subagents_fp: Optional[str] = None,
|
|
568
729
|
) -> None:
|
|
569
|
-
"""Save parsed transcript entries to cache.
|
|
730
|
+
"""Save parsed transcript entries to cache.
|
|
731
|
+
|
|
732
|
+
``subagents_fp`` is the sidecar fingerprint AS OF THE PARSE —
|
|
733
|
+
callers that scanned sidecars should pass the value captured
|
|
734
|
+
before the scan, so a sidecar landing mid-parse mismatches on the
|
|
735
|
+
next read and forces a reparse (over-invalidation; computing it
|
|
736
|
+
here at save time would instead validate a parse that never saw
|
|
737
|
+
the late sidecar). Falls back to computing now when omitted.
|
|
738
|
+
"""
|
|
570
739
|
if self._project_id is None:
|
|
571
740
|
return
|
|
572
741
|
|
|
573
742
|
source_mtime = jsonl_path.stat().st_mtime
|
|
574
743
|
cached_mtime = datetime.now().timestamp()
|
|
744
|
+
if subagents_fp is None:
|
|
745
|
+
subagents_fp = subagents_fingerprint(jsonl_path)
|
|
575
746
|
|
|
576
747
|
with self._get_connection() as conn:
|
|
577
748
|
# Insert or update file record
|
|
@@ -579,13 +750,15 @@ class CacheManager:
|
|
|
579
750
|
conn.execute(
|
|
580
751
|
"""
|
|
581
752
|
INSERT INTO cached_files
|
|
582
|
-
(project_id, file_name, file_path, source_mtime, cached_mtime,
|
|
583
|
-
|
|
753
|
+
(project_id, file_name, file_path, source_mtime, cached_mtime,
|
|
754
|
+
message_count, subagents_fingerprint)
|
|
755
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
584
756
|
ON CONFLICT(project_id, file_name) DO UPDATE SET
|
|
585
757
|
file_path = excluded.file_path,
|
|
586
758
|
source_mtime = excluded.source_mtime,
|
|
587
759
|
cached_mtime = excluded.cached_mtime,
|
|
588
|
-
message_count = excluded.message_count
|
|
760
|
+
message_count = excluded.message_count,
|
|
761
|
+
subagents_fingerprint = excluded.subagents_fingerprint
|
|
589
762
|
""",
|
|
590
763
|
(
|
|
591
764
|
self._project_id,
|
|
@@ -594,6 +767,7 @@ class CacheManager:
|
|
|
594
767
|
source_mtime,
|
|
595
768
|
cached_mtime,
|
|
596
769
|
len(entries),
|
|
770
|
+
subagents_fp,
|
|
597
771
|
),
|
|
598
772
|
)
|
|
599
773
|
|
|
@@ -738,21 +912,82 @@ class CacheManager:
|
|
|
738
912
|
if self._project_id is None:
|
|
739
913
|
return []
|
|
740
914
|
|
|
915
|
+
with self._get_connection() as conn:
|
|
916
|
+
return self._get_working_directories(conn)
|
|
917
|
+
|
|
918
|
+
def _get_working_directories(self, conn: sqlite3.Connection) -> List[str]:
|
|
919
|
+
"""get_working_directories() on an already-open connection."""
|
|
920
|
+
rows = conn.execute(
|
|
921
|
+
"SELECT DISTINCT cwd FROM sessions WHERE project_id = ? AND cwd IS NOT NULL",
|
|
922
|
+
(self._project_id,),
|
|
923
|
+
).fetchall()
|
|
924
|
+
return [row["cwd"] for row in rows]
|
|
925
|
+
|
|
926
|
+
def get_modified_files(self, jsonl_files: List[Path]) -> List[Path]:
|
|
927
|
+
"""Get list of JSONL files that need to be reprocessed.
|
|
928
|
+
|
|
929
|
+
Batched equivalent of calling is_file_cached() per file: one SQL
|
|
930
|
+
query fetches every cached row (one connection open instead of
|
|
931
|
+
one per file), and one scandir per parent directory rules out
|
|
932
|
+
subagent-sidecar fingerprints, replacing the per-file query +
|
|
933
|
+
directory probes that dominated freshness-check time on large
|
|
934
|
+
archives (issue #12).
|
|
935
|
+
"""
|
|
936
|
+
if not jsonl_files:
|
|
937
|
+
return []
|
|
938
|
+
if self._project_id is None:
|
|
939
|
+
return list(jsonl_files)
|
|
940
|
+
|
|
741
941
|
with self._get_connection() as conn:
|
|
742
942
|
rows = conn.execute(
|
|
743
|
-
"SELECT
|
|
943
|
+
"SELECT file_name, source_mtime, subagents_fingerprint"
|
|
944
|
+
" FROM cached_files WHERE project_id = ?",
|
|
744
945
|
(self._project_id,),
|
|
745
946
|
).fetchall()
|
|
947
|
+
cached = {row["file_name"]: row for row in rows}
|
|
948
|
+
|
|
949
|
+
# A session file's sidecars live under <parent>/<stem>/subagents,
|
|
950
|
+
# so one scandir of the parent tells us which files can have a
|
|
951
|
+
# non-empty fingerprint without any per-file stat probes.
|
|
952
|
+
subdir_names: Dict[Path, set[str]] = {}
|
|
953
|
+
|
|
954
|
+
def current_fp(jsonl_file: Path) -> str:
|
|
955
|
+
parent = jsonl_file.parent
|
|
956
|
+
if parent.name == "subagents":
|
|
957
|
+
# Agent file in a subagents dir: its sidecars sit next to
|
|
958
|
+
# it, so the full fingerprint scan is required.
|
|
959
|
+
return subagents_fingerprint(jsonl_file)
|
|
960
|
+
names = subdir_names.get(parent)
|
|
961
|
+
if names is None:
|
|
962
|
+
try:
|
|
963
|
+
with os.scandir(parent) as entries:
|
|
964
|
+
names = {e.name for e in entries if e.is_dir()}
|
|
965
|
+
except OSError:
|
|
966
|
+
names = set[str]()
|
|
967
|
+
subdir_names[parent] = names
|
|
968
|
+
return subagents_fingerprint(jsonl_file) if jsonl_file.stem in names else ""
|
|
969
|
+
|
|
970
|
+
modified: List[Path] = []
|
|
971
|
+
for jsonl_file in jsonl_files:
|
|
972
|
+
row = cached.get(jsonl_file.name)
|
|
973
|
+
if row is None:
|
|
974
|
+
modified.append(jsonl_file)
|
|
975
|
+
continue
|
|
976
|
+
|
|
977
|
+
try:
|
|
978
|
+
source_mtime = jsonl_file.stat().st_mtime
|
|
979
|
+
except OSError:
|
|
980
|
+
# Missing file: same outcome as is_file_cached()'s
|
|
981
|
+
# exists() check returning False.
|
|
982
|
+
modified.append(jsonl_file)
|
|
983
|
+
continue
|
|
984
|
+
|
|
985
|
+
if not _cache_row_is_fresh(
|
|
986
|
+
row, source_mtime, lambda file=jsonl_file: current_fp(file)
|
|
987
|
+
):
|
|
988
|
+
modified.append(jsonl_file)
|
|
746
989
|
|
|
747
|
-
return
|
|
748
|
-
|
|
749
|
-
def get_modified_files(self, jsonl_files: List[Path]) -> List[Path]:
|
|
750
|
-
"""Get list of JSONL files that need to be reprocessed."""
|
|
751
|
-
return [
|
|
752
|
-
jsonl_file
|
|
753
|
-
for jsonl_file in jsonl_files
|
|
754
|
-
if not self.is_file_cached(jsonl_file)
|
|
755
|
-
]
|
|
990
|
+
return modified
|
|
756
991
|
|
|
757
992
|
def get_cached_project_data(self) -> Optional[ProjectCache]:
|
|
758
993
|
"""Get the cached project data if available."""
|
|
@@ -775,19 +1010,11 @@ class CacheManager:
|
|
|
775
1010
|
|
|
776
1011
|
cached_files: Dict[str, CachedFileInfo] = {}
|
|
777
1012
|
for row in file_rows:
|
|
778
|
-
# Get session IDs for this file from messages
|
|
779
|
-
session_rows = conn.execute(
|
|
780
|
-
"SELECT DISTINCT session_id FROM messages WHERE file_id = ? AND session_id IS NOT NULL",
|
|
781
|
-
(row["id"],),
|
|
782
|
-
).fetchall()
|
|
783
|
-
session_ids = [r["session_id"] for r in session_rows]
|
|
784
|
-
|
|
785
1013
|
cached_files[row["file_name"]] = CachedFileInfo(
|
|
786
1014
|
file_path=row["file_path"],
|
|
787
1015
|
source_mtime=row["source_mtime"],
|
|
788
1016
|
cached_mtime=row["cached_mtime"],
|
|
789
1017
|
message_count=row["message_count"],
|
|
790
|
-
session_ids=session_ids,
|
|
791
1018
|
)
|
|
792
1019
|
|
|
793
1020
|
# Get sessions
|
|
@@ -813,6 +1040,10 @@ class CacheManager:
|
|
|
813
1040
|
team_name=row["team_name"] if "team_name" in row.keys() else None,
|
|
814
1041
|
)
|
|
815
1042
|
|
|
1043
|
+
# On the same connection: calling the public method here
|
|
1044
|
+
# would open a second connection per call.
|
|
1045
|
+
working_directories = self._get_working_directories(conn)
|
|
1046
|
+
|
|
816
1047
|
return ProjectCache(
|
|
817
1048
|
version=project_row["version"],
|
|
818
1049
|
cache_created=project_row["cache_created"],
|
|
@@ -825,7 +1056,7 @@ class CacheManager:
|
|
|
825
1056
|
total_cache_creation_tokens=project_row["total_cache_creation_tokens"],
|
|
826
1057
|
total_cache_read_tokens=project_row["total_cache_read_tokens"],
|
|
827
1058
|
sessions=sessions,
|
|
828
|
-
working_directories=
|
|
1059
|
+
working_directories=working_directories,
|
|
829
1060
|
earliest_timestamp=project_row["earliest_timestamp"],
|
|
830
1061
|
latest_timestamp=project_row["latest_timestamp"],
|
|
831
1062
|
)
|
|
@@ -855,6 +1086,11 @@ class CacheManager:
|
|
|
855
1086
|
# JSONLs are re-ingested, so the project index keeps showing
|
|
856
1087
|
# the old session-id title until the cache is rebuilt.
|
|
857
1088
|
"1.2.0": "1.3.0",
|
|
1089
|
+
# 1.5.0 filters <local-command-caveat>-wrapped caveat messages
|
|
1090
|
+
# (Claude Code ≥ ~2.1) from session previews: caches built
|
|
1091
|
+
# earlier have the caveat text baked into first_user_message
|
|
1092
|
+
# for affected sessions.
|
|
1093
|
+
"1.4.0": "1.5.0",
|
|
858
1094
|
}
|
|
859
1095
|
|
|
860
1096
|
cache_ver = version.parse(cache_version)
|