cc-session-tools 4.0.2__tar.gz → 4.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cc_session_tools-4.0.2/src/cc_session_tools.egg-info → cc_session_tools-4.1.0}/PKG-INFO +6 -5
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/README.md +5 -4
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/pyproject.toml +1 -1
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccst.py +117 -1
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/config/hooks-bundle.json +0 -6
- cc_session_tools-4.1.0/src/cc_session_tools/lib/command_recovery.py +299 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/hook_registry.py +0 -2
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/paths.py +7 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/sync_notify.py +7 -11
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/session_gc.py +3 -3
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/sessions.py +2 -1
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/move-session/SKILL.md +30 -29
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/move-session/scripts/move_session.py +192 -58
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/update-command-cache/SKILL.md +29 -12
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +52 -22
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0/src/cc_session_tools.egg-info}/PKG-INFO +6 -5
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/SOURCES.txt +4 -2
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_doctor.py +1 -1
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hook_dispatcher.py +6 -1
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hooks_install_zero_arg.py +0 -1
- cc_session_tools-4.1.0/tests/test_ccst_move_session_cli.py +122 -0
- cc_session_tools-4.1.0/tests/test_ccst_telemetry_recover.py +214 -0
- cc_session_tools-4.1.0/tests/test_command_recovery.py +490 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_hooks_install.py +9 -0
- cc_session_tools-4.0.2/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -206
- cc_session_tools-4.0.2/src/hooks/pending_rename.py +0 -37
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/LICENSE +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/setup.cfg +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/__init__.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/__init__.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccd.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccmsg.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccr.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccs.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccsched.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/config/api-keys.example +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/config/session-tag-hook.json +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/hooks_install.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/__init__.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/api_keys.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/bun_tools.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/claude_flags.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/claude_md_install.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/claude_md_store.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/context_overrides.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/db.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/debug.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/doctor.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/install_sync.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/levenshtein.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/machine_identity.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/lock.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/message.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/repository.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/retention.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/service.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/session_ref.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/store.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/occupancy.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/backup.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/classify.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/dump.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/formats.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/ids.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/importers.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/init_service.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/migrate_uuid.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/naming.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/project_claude_md.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/readiness.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/rehydrate.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/rename_group.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/repository.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/resolve.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/service.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/store.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/sync_check.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/sync_lock.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/vector_clock.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/vector_clock_store.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/verify.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/picker.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/proc_lock.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/prompts.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/roots.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/rules.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/digest.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/due.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/ledger.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/state.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/store.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/surface.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/worker.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/sessions_db.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/sessions_repair.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/shell_install.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/tasklist.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/telemetry_store.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/text_wrap.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/timefmt.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/clean-hook-sessions/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/clean-hook-sessions/scripts/clean-hook-sessions.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/compact-smart/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/extract-pptx/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/extract-pptx/scripts/extract_pptx.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/extract-to-text/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/extract-to-text/scripts/extract_to_text.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/pm-pdata-design-schema/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/pm-pdata-do-audit-and-prepare-to-migrate/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/pm-pdata-do-init/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/pm-pdata-do-manual-migration/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/pm-pdata-do-update-consuming-skills/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/pm-pdata-do-update-project-docs/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/pm-pdata-resolve-conflicts/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/requires.txt +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/top_level.txt +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/__init__.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/attribution.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/cache.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/ccusage_wrapper.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/data/pricing.json +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/parent_inference.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/parser.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/pricing.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/query.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/report.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/schema.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/session_names.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/__init__.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/after_response.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/bash_hard_deny.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/bash_security_review.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/cache.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/catchup.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/confirm_8digit.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/context_window_warning.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/last_screenshot.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/marker_allow.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/markers.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/messaging_deliver.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/model_info.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/normalise.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/pdata_sync.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/pending_migration.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/script_allowlist.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/session_tag.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/stats.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/telemetry.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/telemetry_query.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/telemetry_trim.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/transcript.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/worklog_guard.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_after_response.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_api_keys.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_attribution.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_bash_hard_deny.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_bash_security_review.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_bash_security_review_allowlist.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_bun_tools.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cache.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cache_sqlite.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccr_orphans.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_emptiness.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_eta.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_list_mode.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_scope_flags.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_sentinel_sort.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_session_counts.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_bundle_discovery.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_context_override_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_gc_prune.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_gc_report.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hooks_allowlist.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hooks_install_prune.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hooks_uninstall.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_install_everything.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_machine_identity_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_main_gate.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_init_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_migrate_uuid_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_readiness_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_rename_group_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_sync_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_verify_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_repair_sessions_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_sessions_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_shell_install.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_skills_install.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_skills_uninstall.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_telemetry_query.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_telemetry_trim.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccusage_wrapper.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_claude_flags.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_claude_md_drift.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_claude_md_install.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_clean_hook_sessions_format.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_api_launchers.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_ccd.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_ccd_dry_run.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_ccr.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_ccs.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_version.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_confirm_8digit.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_context_overrides.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_context_window_warning.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_date_format_conformance.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_db.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_debug.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_doctor_mutes.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_empty_session.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_hook_cache.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_hooks_install_records.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_install_everything_migration_banner.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_install_sync.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_last_screenshot.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_levenshtein.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_machine_identity.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_marker_allow.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_migrate_ccmsg.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_migrate_ccsched.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_migrate_sessions_db.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_migrate_telemetry.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_model_info.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_normalise.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_occupancy.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_parent_inference.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_parser.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_paths.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pdata_audit_skill.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pdata_post_migration_skills.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pdata_uuid_migration_runbook.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pending_migration_hook.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_picker.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pricing.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_proc_lock.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_prompts.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_query.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_report.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_roots.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_rules.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_scheduler_bundled_jobs.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_schema.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_script_allowlist.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_session_names.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_session_tag.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_sessions.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_sessions_db.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_sessions_db_migration_chain.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_sessions_repair.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_tasklist.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_telemetry.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_telemetry_store.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_text_wrap.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_timefmt.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_transcript.py +0 -0
- {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_worklog_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cc-session-tools
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (hooks package)
|
|
5
5
|
Author: raffishquartan
|
|
6
6
|
License: MIT License
|
|
@@ -422,8 +422,7 @@ These keep the rest of CCST working without any action from you:
|
|
|
422
422
|
- `messaging-deliver` - delivers `ccmsg` messages at session start and each prompt.
|
|
423
423
|
- `catchup` - catches up missed scheduled jobs and shows the digest.
|
|
424
424
|
- `pdata-sync` - syncs `pdata` databases across your machines (see below).
|
|
425
|
-
- `pending-migration`
|
|
426
|
-
half-finished session renames.
|
|
425
|
+
- `pending-migration` - surfaces unfinished data migrations.
|
|
427
426
|
- `last-screenshot` - type `>lss` in a prompt and Claude gets the path of your newest
|
|
428
427
|
screenshot (set `CCST_SCREENSHOT_DIR`; several directories may be separated by `:`).
|
|
429
428
|
- `marker-allow` - lets skills drop small marker files without a permission prompt.
|
|
@@ -560,7 +559,7 @@ flag.
|
|
|
560
559
|
| `sessions` | inspect `sessions.db`; one-shot schema migrations |
|
|
561
560
|
| `repair` | repair known `sessions.db` corruption |
|
|
562
561
|
| `gc` | `report` and `prune` orphaned per-session data |
|
|
563
|
-
| `telemetry` | `query` and `trim` the hook
|
|
562
|
+
| `telemetry` | `query` and `trim` the hook log; `recover` a command from its hash |
|
|
564
563
|
| `migrate` | one-shot migrations from legacy flat-file stores to SQLite |
|
|
565
564
|
| `context-override` | toggle the context-window warning for the current session |
|
|
566
565
|
|
|
@@ -607,7 +606,9 @@ CCST is deliberately not a server or a shared team tool. It is local infrastruct
|
|
|
607
606
|
person running Claude Code hard, on one or more of their own machines. Everything is SQLite
|
|
608
607
|
under `~/.local/share/claude/`, one file per subsystem (sessions, messages, scheduler,
|
|
609
608
|
telemetry, command cache), and all of it is inspectable with `ccst telemetry query`,
|
|
610
|
-
`ccst gc report`, `ccst sessions list`, or a plain `sqlite3` shell.
|
|
609
|
+
`ccst gc report`, `ccst sessions list`, or a plain `sqlite3` shell. Telemetry stores only a
|
|
610
|
+
hash of each reviewed command; `ccst telemetry recover <hash>` finds the command behind it in
|
|
611
|
+
the firing session's transcripts. `ccst doctor` is the one
|
|
611
612
|
command that checks all of it at once. Run it any time something feels off.
|
|
612
613
|
|
|
613
614
|
## Contributing
|
|
@@ -363,8 +363,7 @@ These keep the rest of CCST working without any action from you:
|
|
|
363
363
|
- `messaging-deliver` - delivers `ccmsg` messages at session start and each prompt.
|
|
364
364
|
- `catchup` - catches up missed scheduled jobs and shows the digest.
|
|
365
365
|
- `pdata-sync` - syncs `pdata` databases across your machines (see below).
|
|
366
|
-
- `pending-migration`
|
|
367
|
-
half-finished session renames.
|
|
366
|
+
- `pending-migration` - surfaces unfinished data migrations.
|
|
368
367
|
- `last-screenshot` - type `>lss` in a prompt and Claude gets the path of your newest
|
|
369
368
|
screenshot (set `CCST_SCREENSHOT_DIR`; several directories may be separated by `:`).
|
|
370
369
|
- `marker-allow` - lets skills drop small marker files without a permission prompt.
|
|
@@ -501,7 +500,7 @@ flag.
|
|
|
501
500
|
| `sessions` | inspect `sessions.db`; one-shot schema migrations |
|
|
502
501
|
| `repair` | repair known `sessions.db` corruption |
|
|
503
502
|
| `gc` | `report` and `prune` orphaned per-session data |
|
|
504
|
-
| `telemetry` | `query` and `trim` the hook
|
|
503
|
+
| `telemetry` | `query` and `trim` the hook log; `recover` a command from its hash |
|
|
505
504
|
| `migrate` | one-shot migrations from legacy flat-file stores to SQLite |
|
|
506
505
|
| `context-override` | toggle the context-window warning for the current session |
|
|
507
506
|
|
|
@@ -548,7 +547,9 @@ CCST is deliberately not a server or a shared team tool. It is local infrastruct
|
|
|
548
547
|
person running Claude Code hard, on one or more of their own machines. Everything is SQLite
|
|
549
548
|
under `~/.local/share/claude/`, one file per subsystem (sessions, messages, scheduler,
|
|
550
549
|
telemetry, command cache), and all of it is inspectable with `ccst telemetry query`,
|
|
551
|
-
`ccst gc report`, `ccst sessions list`, or a plain `sqlite3` shell.
|
|
550
|
+
`ccst gc report`, `ccst sessions list`, or a plain `sqlite3` shell. Telemetry stores only a
|
|
551
|
+
hash of each reviewed command; `ccst telemetry recover <hash>` finds the command behind it in
|
|
552
|
+
the firing session's transcripts. `ccst doctor` is the one
|
|
552
553
|
command that checks all of it at once. Run it any time something feels off.
|
|
553
554
|
|
|
554
555
|
## Contributing
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cc-session-tools"
|
|
7
|
-
version = "4.0
|
|
7
|
+
version = "4.1.0"
|
|
8
8
|
description = "Claude Code session management CLI tools (ccd, ccr, ccs, claude-code-usage, ccst) and hook library (hooks package)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -29,6 +29,10 @@ Current subcommands:
|
|
|
29
29
|
telemetry trim Trim telemetry.db by size / age (see ccst telemetry trim --help).
|
|
30
30
|
telemetry query Query recent hook fires from telemetry.db (see
|
|
31
31
|
ccst telemetry query --help).
|
|
32
|
+
telemetry recover <hash>... Recover the Bash command behind each bash-security-review
|
|
33
|
+
input_hash from the firing sessions' transcripts, one
|
|
34
|
+
compact line per hash (requires CCST_FIRES_ACCESS=1; see
|
|
35
|
+
ccst telemetry recover --help).
|
|
32
36
|
gc report Report orphaned per-session-uuid entries across the
|
|
33
37
|
scheduler, messaging, and session-env stores (never
|
|
34
38
|
deletes anything).
|
|
@@ -106,6 +110,7 @@ import datetime
|
|
|
106
110
|
import importlib
|
|
107
111
|
import json
|
|
108
112
|
import os
|
|
113
|
+
import subprocess
|
|
109
114
|
import sys
|
|
110
115
|
from dataclasses import dataclass
|
|
111
116
|
from enum import Enum
|
|
@@ -119,7 +124,7 @@ from cc_session_tools.hooks_install import (
|
|
|
119
124
|
prune_stale_hooks,
|
|
120
125
|
write_json_atomic,
|
|
121
126
|
)
|
|
122
|
-
from cc_session_tools.lib import machine_identity
|
|
127
|
+
from cc_session_tools.lib import command_recovery, machine_identity
|
|
123
128
|
from cc_session_tools.lib.claude_md_install import SECTION_IDS as CLAUDE_MD_SECTION_IDS
|
|
124
129
|
from cc_session_tools.lib.claude_md_install import DRIFT_WARNING_MARKER, MarkdownAction, MarkdownResult
|
|
125
130
|
from cc_session_tools.lib.hook_registry import HOOK_DESCRIPTIONS, HOOK_VERBS
|
|
@@ -998,6 +1003,43 @@ def _cmd_telemetry_query(args: argparse.Namespace) -> int:
|
|
|
998
1003
|
return query_main(argv)
|
|
999
1004
|
|
|
1000
1005
|
|
|
1006
|
+
def _non_negative_int(raw: str) -> int:
|
|
1007
|
+
"""argparse type for a count that may be 0, else a usage error naming it."""
|
|
1008
|
+
try:
|
|
1009
|
+
value = int(raw)
|
|
1010
|
+
except ValueError:
|
|
1011
|
+
raise argparse.ArgumentTypeError(f"{raw!r} is not an integer") from None
|
|
1012
|
+
if value < 0:
|
|
1013
|
+
raise argparse.ArgumentTypeError(f"{raw!r} is negative")
|
|
1014
|
+
return value
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _cmd_telemetry_recover(args: argparse.Namespace) -> int:
|
|
1018
|
+
# A self-imposed convention shared with update-command-cache: bash-hard-deny
|
|
1019
|
+
# does not match this verb, so the variable is enforced here alone.
|
|
1020
|
+
if os.environ.get("CCST_FIRES_ACCESS") != "1":
|
|
1021
|
+
print(
|
|
1022
|
+
"Refusing to read telemetry.db and transcripts without CCST_FIRES_ACCESS=1.\n"
|
|
1023
|
+
"Re-run as: CCST_FIRES_ACCESS=1 ccst telemetry recover <hash>...",
|
|
1024
|
+
file=sys.stderr,
|
|
1025
|
+
)
|
|
1026
|
+
return 2
|
|
1027
|
+
try:
|
|
1028
|
+
hashes = command_recovery.normalise_args(args.hashes)
|
|
1029
|
+
except command_recovery.MalformedHashError as exc:
|
|
1030
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
1031
|
+
return 2
|
|
1032
|
+
results = command_recovery.recover(
|
|
1033
|
+
hashes,
|
|
1034
|
+
projects_dir=Path(args.projects_dir) if args.projects_dir else None,
|
|
1035
|
+
hooks_dir=Path(args.hooks_dir) if args.hooks_dir else None,
|
|
1036
|
+
scan_all=args.scan_all,
|
|
1037
|
+
)
|
|
1038
|
+
for rec in results:
|
|
1039
|
+
print(command_recovery.format_line(rec, args.max_chars))
|
|
1040
|
+
return 0
|
|
1041
|
+
|
|
1042
|
+
|
|
1001
1043
|
# ---------- gc report ----------
|
|
1002
1044
|
|
|
1003
1045
|
|
|
@@ -3260,6 +3302,40 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
3260
3302
|
help="telemetry.db directory (default: CCST_HOOKS_DIR or ~/.local/share/claude/)",
|
|
3261
3303
|
)
|
|
3262
3304
|
|
|
3305
|
+
telemetry_recover_parser = telemetry_sub.add_parser(
|
|
3306
|
+
"recover",
|
|
3307
|
+
help="Recover the Bash command behind bash-security-review input hashes",
|
|
3308
|
+
description=(
|
|
3309
|
+
"For each hash (full, or a prefix of at least 8 hex characters; a sha256: "
|
|
3310
|
+
"prefix is accepted) print one tab-separated line: hash as given (normalised), "
|
|
3311
|
+
"status (command|session|ambiguous|unknown), full hash (command only), session "
|
|
3312
|
+
"uuid, <transcript>:<line>, command. A command is printed only when it "
|
|
3313
|
+
"re-hashes to the given hash. Searches the firing sessions' main and subagent "
|
|
3314
|
+
"transcripts. Requires CCST_FIRES_ACCESS=1."
|
|
3315
|
+
),
|
|
3316
|
+
)
|
|
3317
|
+
telemetry_recover_parser.add_argument(
|
|
3318
|
+
"hashes", nargs="+", metavar="HASH", help="input_hash, full or a prefix of 8+ hex characters",
|
|
3319
|
+
)
|
|
3320
|
+
telemetry_recover_parser.add_argument(
|
|
3321
|
+
"--max-chars", type=_non_negative_int, default=command_recovery.DEFAULT_MAX_CHARS,
|
|
3322
|
+
metavar="N",
|
|
3323
|
+
help="Cut each escaped command to N characters, marking how many were cut "
|
|
3324
|
+
f"(default: {command_recovery.DEFAULT_MAX_CHARS}; 0 for no limit)",
|
|
3325
|
+
)
|
|
3326
|
+
telemetry_recover_parser.add_argument(
|
|
3327
|
+
"--scan-all", action="store_true",
|
|
3328
|
+
help="Search every transcript, not only those of sessions that fired the hash",
|
|
3329
|
+
)
|
|
3330
|
+
telemetry_recover_parser.add_argument(
|
|
3331
|
+
"--projects-dir", default=None, metavar="DIR",
|
|
3332
|
+
help="Transcript root (default: ~/.claude/projects)",
|
|
3333
|
+
)
|
|
3334
|
+
telemetry_recover_parser.add_argument(
|
|
3335
|
+
"--hooks-dir", default=None, metavar="DIR",
|
|
3336
|
+
help="telemetry.db directory (default: CCST_HOOKS_DIR or ~/.local/share/claude/)",
|
|
3337
|
+
)
|
|
3338
|
+
|
|
3263
3339
|
# ---- gc ----
|
|
3264
3340
|
gc_parser = sub.add_parser("gc", help="Garbage-collection reports for per-session-uuid stores")
|
|
3265
3341
|
gc_sub = gc_parser.add_subparsers(dest="verb", metavar="<verb>")
|
|
@@ -3941,10 +4017,48 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
3941
4017
|
"action", nargs="?", default="on", choices=["on", "off", "status"]
|
|
3942
4018
|
)
|
|
3943
4019
|
|
|
4020
|
+
# ---- move-session ----
|
|
4021
|
+
# Listed here for the help output only: main() forwards `ccst move-session
|
|
4022
|
+
# ...` to the bundled script before argparse runs (see _cmd_move_session).
|
|
4023
|
+
sub.add_parser(
|
|
4024
|
+
"move-session",
|
|
4025
|
+
add_help=False,
|
|
4026
|
+
help="Move and/or rename a Claude Code session (the move-session skill's script; "
|
|
4027
|
+
"`ccst move-session --help` for its options)",
|
|
4028
|
+
)
|
|
4029
|
+
|
|
3944
4030
|
return parser
|
|
3945
4031
|
|
|
3946
4032
|
|
|
4033
|
+
MOVE_SESSION_NOUN = "move-session"
|
|
4034
|
+
|
|
4035
|
+
|
|
4036
|
+
def _cmd_move_session(script_args: list[str]) -> int:
|
|
4037
|
+
"""Run the bundled move-session script with ``script_args`` unchanged,
|
|
4038
|
+
under this interpreter (the one that has cc-session-tools installed), and
|
|
4039
|
+
return its exit code.
|
|
4040
|
+
|
|
4041
|
+
Dispatched before argparse because the script owns its own command line:
|
|
4042
|
+
a subparser could not forward a call whose first argument is an option
|
|
4043
|
+
(every real call starts with --src-session), and ``--help`` would print
|
|
4044
|
+
ccst's help instead of the script's. A subprocess keeps the script's
|
|
4045
|
+
sys.argv, argparse prog and module state its own.
|
|
4046
|
+
|
|
4047
|
+
Not exempt from install auto-sync, like any ordinary ccst command.
|
|
4048
|
+
"""
|
|
4049
|
+
from cc_session_tools.lib import install_sync
|
|
4050
|
+
|
|
4051
|
+
install_sync.ensure_synced(
|
|
4052
|
+
noun=MOVE_SESSION_NOUN, verb=None, installed_version=__version__
|
|
4053
|
+
)
|
|
4054
|
+
script = _discover_source_dir() / "move-session" / "scripts" / "move_session.py"
|
|
4055
|
+
return subprocess.run([sys.executable, str(script), *script_args]).returncode
|
|
4056
|
+
|
|
4057
|
+
|
|
3947
4058
|
def main() -> None:
|
|
4059
|
+
if sys.argv[1:2] == [MOVE_SESSION_NOUN]:
|
|
4060
|
+
sys.exit(_cmd_move_session(sys.argv[2:]))
|
|
4061
|
+
|
|
3948
4062
|
parser = _build_parser()
|
|
3949
4063
|
args = parser.parse_args()
|
|
3950
4064
|
|
|
@@ -4011,6 +4125,8 @@ def main() -> None:
|
|
|
4011
4125
|
sys.exit(_cmd_telemetry_trim(args))
|
|
4012
4126
|
if args.verb == "query":
|
|
4013
4127
|
sys.exit(_cmd_telemetry_query(args))
|
|
4128
|
+
if args.verb == "recover":
|
|
4129
|
+
sys.exit(_cmd_telemetry_recover(args))
|
|
4014
4130
|
|
|
4015
4131
|
if args.noun == "gc":
|
|
4016
4132
|
if args.verb == "report":
|
{cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/config/hooks-bundle.json
RENAMED
|
@@ -27,12 +27,6 @@
|
|
|
27
27
|
"timeout": 5,
|
|
28
28
|
"statusMessage": "Checking for unmigrated legacy data stores..."
|
|
29
29
|
},
|
|
30
|
-
{
|
|
31
|
-
"type": "command",
|
|
32
|
-
"command": "ccst hooks run pending-rename",
|
|
33
|
-
"timeout": 10,
|
|
34
|
-
"statusMessage": "Checking for pending session-renames..."
|
|
35
|
-
},
|
|
36
30
|
{
|
|
37
31
|
"type": "command",
|
|
38
32
|
"command": "ccst hooks run pdata-sync",
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Recover the Bash command behind a bash-security-review ``input_hash``.
|
|
2
|
+
|
|
3
|
+
telemetry.db stores only ``sha256:<hex>`` of each reviewed command, never its
|
|
4
|
+
text. The same string is the ``input.command`` of a ``Bash`` tool_use record in
|
|
5
|
+
a transcript, so the command can be recovered by hashing each Bash command in
|
|
6
|
+
the transcripts of the sessions that fired the hash and keeping exact SHA-256
|
|
7
|
+
matches. Nothing is guessed: a hash with no exact match is reported as the
|
|
8
|
+
session it fired in (``session``), as ``ambiguous`` for a prefix matching
|
|
9
|
+
several hashes, or as ``unknown``.
|
|
10
|
+
|
|
11
|
+
Transcripts searched for session S, under the projects directory
|
|
12
|
+
(``~/.claude/projects`` by default): every ``<key>/S.jsonl`` and every
|
|
13
|
+
``<key>/S/subagents/*.jsonl`` (the hook's ``session_id`` for a subagent's
|
|
14
|
+
command is the parent session's uuid). With ``scan_all`` every transcript is
|
|
15
|
+
searched instead. Each file is read at most once per call to ``recover``.
|
|
16
|
+
|
|
17
|
+
Backs ``ccst telemetry recover`` and the update-command-cache skill's ``list``
|
|
18
|
+
previews. Reads only; writes to no store.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import enum
|
|
23
|
+
import glob
|
|
24
|
+
import hashlib
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
import unicodedata
|
|
28
|
+
from collections.abc import Iterable, Iterator, Sequence
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from cc_session_tools.lib import db, paths, telemetry_store
|
|
33
|
+
|
|
34
|
+
DEFAULT_MAX_CHARS = 200
|
|
35
|
+
MIN_PREFIX_LEN = 8
|
|
36
|
+
FULL_HASH_LEN = 64
|
|
37
|
+
|
|
38
|
+
_SHA_PREFIX = "sha256:"
|
|
39
|
+
_HEX_RE = re.compile(r"[0-9a-f]+")
|
|
40
|
+
# Claude Code writes transcripts as compact JSON, so every Bash tool_use line
|
|
41
|
+
# contains this substring; lines without it are skipped before json.loads.
|
|
42
|
+
_BASH_MARKER = b'"name":"Bash"'
|
|
43
|
+
_HOOK = "bash-security-review"
|
|
44
|
+
_SIMPLE_ESCAPES = {"\t": "\\t", "\r": "\\r", "\n": "\\n"}
|
|
45
|
+
# Control characters (C0, DEL, C1) and the line and paragraph separators.
|
|
46
|
+
_ESCAPED_CATEGORIES = frozenset({"Cc", "Zl", "Zp"})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RecoveryStatus(enum.Enum):
|
|
50
|
+
COMMAND = "command"
|
|
51
|
+
SESSION = "session"
|
|
52
|
+
AMBIGUOUS = "ambiguous"
|
|
53
|
+
UNKNOWN = "unknown"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MalformedHashError(ValueError):
|
|
57
|
+
"""A hash argument that is not 8-64 hex characters after normalisation."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, arg: str) -> None:
|
|
60
|
+
super().__init__(
|
|
61
|
+
f"malformed hash argument {arg!r}: expected {MIN_PREFIX_LEN}-{FULL_HASH_LEN} "
|
|
62
|
+
f"hex characters, optionally prefixed with {_SHA_PREFIX!r}"
|
|
63
|
+
)
|
|
64
|
+
self.arg = arg
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, slots=True)
|
|
68
|
+
class Recovery:
|
|
69
|
+
"""The outcome for one normalised hash argument.
|
|
70
|
+
|
|
71
|
+
``full_hash`` and ``command`` are set only for ``COMMAND``; ``location`` is
|
|
72
|
+
``<path>:<line>`` for ``COMMAND``, the session's transcript path (or empty)
|
|
73
|
+
for ``SESSION``, and empty otherwise.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
arg: str
|
|
77
|
+
status: RecoveryStatus
|
|
78
|
+
full_hash: str
|
|
79
|
+
session_id: str
|
|
80
|
+
location: str
|
|
81
|
+
command: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True, slots=True)
|
|
85
|
+
class _Match:
|
|
86
|
+
path: Path
|
|
87
|
+
line: int
|
|
88
|
+
command: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def normalise_hash_arg(raw: str) -> str:
|
|
92
|
+
"""Lower-case, strip an optional ``sha256:`` prefix, and require 8-64 hex
|
|
93
|
+
characters. Raises MalformedHashError naming ``raw`` otherwise."""
|
|
94
|
+
value = raw.lower()
|
|
95
|
+
if value.startswith(_SHA_PREFIX):
|
|
96
|
+
value = value[len(_SHA_PREFIX):]
|
|
97
|
+
if not MIN_PREFIX_LEN <= len(value) <= FULL_HASH_LEN or not _HEX_RE.fullmatch(value):
|
|
98
|
+
raise MalformedHashError(raw)
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def normalise_args(raws: Iterable[str]) -> list[str]:
|
|
103
|
+
"""Normalise every argument (raising on the first malformed one) and
|
|
104
|
+
collapse duplicates, keeping first-seen order."""
|
|
105
|
+
seen: dict[str, None] = {}
|
|
106
|
+
for raw in raws:
|
|
107
|
+
seen.setdefault(normalise_hash_arg(raw), None)
|
|
108
|
+
return list(seen)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _escape_char(ch: str) -> str:
|
|
112
|
+
if ch in _SIMPLE_ESCAPES:
|
|
113
|
+
return _SIMPLE_ESCAPES[ch]
|
|
114
|
+
if unicodedata.category(ch) in _ESCAPED_CATEGORIES:
|
|
115
|
+
return f"\\u{ord(ch):04x}"
|
|
116
|
+
return ch
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def escape_command(command: str) -> str:
|
|
120
|
+
"""One-line form of ``command``: tab, CR, LF as ``\\t``, ``\\r``, ``\\n``;
|
|
121
|
+
every other control character (C0, DEL, C1) and the Unicode line and
|
|
122
|
+
paragraph separators as ``\\uXXXX``; everything else literal."""
|
|
123
|
+
return "".join(_escape_char(ch) for ch in command)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def format_command(command: str, max_chars: int) -> str:
|
|
127
|
+
"""Escape ``command`` and, if the escaped text is longer than ``max_chars``
|
|
128
|
+
(0 = no limit), cut it without splitting an escape sequence and append
|
|
129
|
+
``…[+N chars]``, N being the number of escaped characters cut."""
|
|
130
|
+
tokens = [_escape_char(ch) for ch in command]
|
|
131
|
+
total = sum(len(t) for t in tokens)
|
|
132
|
+
if max_chars == 0 or total <= max_chars:
|
|
133
|
+
return "".join(tokens)
|
|
134
|
+
kept: list[str] = []
|
|
135
|
+
used = 0
|
|
136
|
+
for token in tokens:
|
|
137
|
+
if used + len(token) > max_chars:
|
|
138
|
+
break
|
|
139
|
+
kept.append(token)
|
|
140
|
+
used += len(token)
|
|
141
|
+
return "".join(kept) + f"…[+{total - used} chars]"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def format_line(rec: Recovery, max_chars: int) -> str:
|
|
145
|
+
"""The tab-separated output line for one Recovery."""
|
|
146
|
+
command = format_command(rec.command, max_chars) if rec.command else ""
|
|
147
|
+
return "\t".join(
|
|
148
|
+
[rec.arg, rec.status.value, rec.full_hash, rec.session_id, rec.location, command]
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def iter_bash_commands(path: Path) -> Iterator[tuple[int, str]]:
|
|
153
|
+
"""Yield (1-based line number, command) for every Bash tool_use in one
|
|
154
|
+
transcript file. A line that is not valid UTF-8 or JSON (a transcript still
|
|
155
|
+
being written can end mid-record) or whose command cannot be hashed (a lone
|
|
156
|
+
surrogate) cannot hold a recoverable command and is skipped."""
|
|
157
|
+
with path.open("rb") as fh:
|
|
158
|
+
for lineno, raw in enumerate(fh, start=1):
|
|
159
|
+
if _BASH_MARKER not in raw:
|
|
160
|
+
continue
|
|
161
|
+
try:
|
|
162
|
+
record = json.loads(raw.decode("utf-8"))
|
|
163
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
164
|
+
continue
|
|
165
|
+
for command in _bash_commands_in(record):
|
|
166
|
+
try:
|
|
167
|
+
command.encode()
|
|
168
|
+
except UnicodeEncodeError:
|
|
169
|
+
continue
|
|
170
|
+
yield lineno, command
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _bash_commands_in(record: object) -> Iterator[str]:
|
|
174
|
+
if not isinstance(record, dict):
|
|
175
|
+
return
|
|
176
|
+
message = record.get("message")
|
|
177
|
+
if not isinstance(message, dict):
|
|
178
|
+
return
|
|
179
|
+
content = message.get("content")
|
|
180
|
+
if not isinstance(content, list):
|
|
181
|
+
return
|
|
182
|
+
for block in content:
|
|
183
|
+
if (
|
|
184
|
+
isinstance(block, dict)
|
|
185
|
+
and block.get("type") == "tool_use"
|
|
186
|
+
and block.get("name") == "Bash"
|
|
187
|
+
and isinstance(block.get("input"), dict)
|
|
188
|
+
and isinstance(block["input"].get("command"), str)
|
|
189
|
+
):
|
|
190
|
+
yield block["input"]["command"]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def session_of(path: Path) -> str:
|
|
194
|
+
"""The session uuid a transcript belongs to: its stem for a main
|
|
195
|
+
transcript, the parent session directory for a subagent transcript."""
|
|
196
|
+
if path.parent.name == "subagents":
|
|
197
|
+
return path.parent.parent.name
|
|
198
|
+
return path.stem
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def session_transcripts(projects_dir: Path, session_id: str) -> list[Path]:
|
|
202
|
+
"""Main transcript(s) first, then subagent transcripts, each sorted."""
|
|
203
|
+
pattern = glob.escape(session_id)
|
|
204
|
+
return sorted(projects_dir.glob(f"*/{pattern}.jsonl")) + sorted(
|
|
205
|
+
projects_dir.glob(f"*/{pattern}/subagents/*.jsonl")
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def all_transcripts(projects_dir: Path) -> list[Path]:
|
|
210
|
+
return sorted(projects_dir.glob("*/*.jsonl")) + sorted(
|
|
211
|
+
projects_dir.glob("*/*/subagents/*.jsonl")
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _fired_sessions(hooks_dir: Path | None) -> dict[str, dict[str, str]]:
|
|
216
|
+
"""{full hash: {session_id: latest ts}} for every bash-security-review fire."""
|
|
217
|
+
db_file = telemetry_store.db_path(hooks_dir)
|
|
218
|
+
if not db_file.is_file():
|
|
219
|
+
return {}
|
|
220
|
+
conn = db.connect(db_file, readonly=True)
|
|
221
|
+
try:
|
|
222
|
+
rows = conn.execute(
|
|
223
|
+
"SELECT input_hash, session_id, MAX(ts) FROM telemetry_events "
|
|
224
|
+
"WHERE hook = ? GROUP BY input_hash, session_id",
|
|
225
|
+
(_HOOK,),
|
|
226
|
+
).fetchall()
|
|
227
|
+
finally:
|
|
228
|
+
conn.close()
|
|
229
|
+
fired: dict[str, dict[str, str]] = {}
|
|
230
|
+
for input_hash, session_id, ts in rows:
|
|
231
|
+
full = input_hash[len(_SHA_PREFIX):] if input_hash.startswith(_SHA_PREFIX) else input_hash
|
|
232
|
+
fired.setdefault(full, {})[session_id] = ts
|
|
233
|
+
return fired
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _scan(files: Sequence[Path], args: Sequence[str]) -> dict[str, _Match]:
|
|
237
|
+
"""Read each file once; return the first occurrence of every command whose
|
|
238
|
+
hash starts with one of ``args``."""
|
|
239
|
+
by_len: dict[int, set[str]] = {}
|
|
240
|
+
for arg in args:
|
|
241
|
+
by_len.setdefault(len(arg), set()).add(arg)
|
|
242
|
+
found: dict[str, _Match] = {}
|
|
243
|
+
for path in files:
|
|
244
|
+
for lineno, command in iter_bash_commands(path):
|
|
245
|
+
digest = hashlib.sha256(command.encode()).hexdigest()
|
|
246
|
+
if digest in found:
|
|
247
|
+
continue
|
|
248
|
+
if any(digest[:n] in prefixes for n, prefixes in by_len.items()):
|
|
249
|
+
found[digest] = _Match(path, lineno, command)
|
|
250
|
+
return found
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def recover(
|
|
254
|
+
args: Sequence[str],
|
|
255
|
+
*,
|
|
256
|
+
projects_dir: Path | None = None,
|
|
257
|
+
hooks_dir: Path | None = None,
|
|
258
|
+
scan_all: bool = False,
|
|
259
|
+
) -> list[Recovery]:
|
|
260
|
+
"""One Recovery per argument, in order. ``args`` must already be
|
|
261
|
+
normalised (see normalise_args)."""
|
|
262
|
+
projects = projects_dir if projects_dir is not None else paths.claude_projects_dir()
|
|
263
|
+
fired = _fired_sessions(hooks_dir)
|
|
264
|
+
fired_matches = {arg: sorted(h for h in fired if h.startswith(arg)) for arg in args}
|
|
265
|
+
|
|
266
|
+
if scan_all:
|
|
267
|
+
files = all_transcripts(projects)
|
|
268
|
+
else:
|
|
269
|
+
sessions = sorted(
|
|
270
|
+
{s for hashes in fired_matches.values() for h in hashes for s in fired[h]}
|
|
271
|
+
)
|
|
272
|
+
files = [f for s in sessions for f in session_transcripts(projects, s)]
|
|
273
|
+
found = _scan(files, args)
|
|
274
|
+
|
|
275
|
+
results: list[Recovery] = []
|
|
276
|
+
for arg in args:
|
|
277
|
+
candidates = set(fired_matches[arg])
|
|
278
|
+
if scan_all:
|
|
279
|
+
candidates.update(h for h in found if h.startswith(arg))
|
|
280
|
+
if not candidates:
|
|
281
|
+
results.append(Recovery(arg, RecoveryStatus.UNKNOWN, "", "", "", ""))
|
|
282
|
+
continue
|
|
283
|
+
if len(candidates) > 1:
|
|
284
|
+
results.append(Recovery(arg, RecoveryStatus.AMBIGUOUS, "", "", "", ""))
|
|
285
|
+
continue
|
|
286
|
+
[full] = candidates
|
|
287
|
+
match = found.get(full)
|
|
288
|
+
if match is not None:
|
|
289
|
+
results.append(Recovery(
|
|
290
|
+
arg, RecoveryStatus.COMMAND, full, session_of(match.path),
|
|
291
|
+
f"{match.path}:{match.line}", match.command,
|
|
292
|
+
))
|
|
293
|
+
continue
|
|
294
|
+
session_ts = fired[full]
|
|
295
|
+
latest = max(session_ts, key=lambda s: (session_ts[s], s))
|
|
296
|
+
transcripts = session_transcripts(projects, latest)
|
|
297
|
+
location = str(transcripts[0]) if transcripts else ""
|
|
298
|
+
results.append(Recovery(arg, RecoveryStatus.SESSION, "", latest, location, ""))
|
|
299
|
+
return results
|
|
@@ -32,7 +32,6 @@ HOOK_VERBS: dict[str, str] = {
|
|
|
32
32
|
"catchup": "hooks.catchup",
|
|
33
33
|
"pending-migration": "hooks.pending_migration",
|
|
34
34
|
"context-window-warning": "hooks.context_window_warning",
|
|
35
|
-
"pending-rename": "hooks.pending_rename",
|
|
36
35
|
"pdata-sync": "hooks.pdata_sync",
|
|
37
36
|
}
|
|
38
37
|
|
|
@@ -50,7 +49,6 @@ HOOK_DESCRIPTIONS: dict[str, str] = {
|
|
|
50
49
|
"catchup": "Reconciles+launches missed scheduled jobs (ccsched) detached and surfaces a catch-up digest (SessionStart + UserPromptSubmit)",
|
|
51
50
|
"pending-migration": "Detects legacy pre-1.0.0 flat-file data left unmigrated (ccmsg/ccsched/sessions/telemetry) and surfaces a FAIL digest pointing at `ccst migrate all`, plus pdata project databases still on format 3 (integer ids) pointing at `ccst pdata migrate-uuid` and the runbook; honours `ccst doctor --mute` (SessionStart)",
|
|
52
51
|
"context-window-warning": "Nudges toward /compact when the context window passes 150k/200k tokens; silenced for the session by /context-override (Stop)",
|
|
53
|
-
"pending-rename": "Surfaces `.pending-rename` markers left by the move-session skill, skipping those whose /rename already landed and collapsing the list above 3; self-times-out at 5s with a visible notice (SessionStart)",
|
|
54
52
|
"pdata-sync": "Multi-laptop `ccst pdata` sync: rehydrates this project's DB from the published dump when it dominates local and the project isn't occupied by another live session (SessionStart), and publishes a fresh dump when local has new writes (SessionEnd); surfaces forks/checksum failures via Telegram + the catch-up digest",
|
|
55
53
|
}
|
|
56
54
|
|
|
@@ -21,3 +21,10 @@ def data_home() -> Path:
|
|
|
21
21
|
if override:
|
|
22
22
|
return Path(override)
|
|
23
23
|
return Path.home() / ".local" / "share" / "claude"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def claude_projects_dir() -> Path:
|
|
27
|
+
"""Claude Code's transcript root, ~/.claude/projects (one <encoded-cwd>
|
|
28
|
+
directory per project, holding <uuid>.jsonl and <uuid>/subagents/*.jsonl).
|
|
29
|
+
Resolved on each call so tests that redirect Path.home() see their own."""
|
|
30
|
+
return Path.home() / ".claude" / "projects"
|
{cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/sync_notify.py
RENAMED
|
@@ -14,17 +14,13 @@ writes to, so a conflict is picked up by the existing `surface.surface()` / `dig
|
|
|
14
14
|
pipeline (the machinery behind `[cc-scheduler] scheduled-task catch-up: ...`) with no second
|
|
15
15
|
digest-plumbing path to maintain.
|
|
16
16
|
|
|
17
|
-
This deliberately does NOT
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
session that just hit a conflict already gets an immediate, in-the-moment
|
|
25
|
-
`hookSpecificOutput`/`systemMessage` straight from the calling hook (see Task 12's
|
|
26
|
-
`hooks.pdata_sync`, which prints its own message using the same JSON protocol as
|
|
27
|
-
`catchup.py`'s `_emit`, exactly like `pending_rename.py` does) — no digest round-trip needed.
|
|
17
|
+
This deliberately does NOT add a dedicated SessionStart channel (a marker file plus its own
|
|
18
|
+
hook script): that would be a third channel with no reusable write-side API, which is exactly
|
|
19
|
+
the "not building either channel" scope this module is meant to stay inside. It would also be
|
|
20
|
+
redundant for the case it might otherwise justify itself for: a live session that just hit a
|
|
21
|
+
conflict already gets an immediate, in-the-moment `hookSpecificOutput`/`systemMessage` straight
|
|
22
|
+
from the calling hook (see Task 12's `hooks.pdata_sync`, which prints its own message using the
|
|
23
|
+
same JSON protocol as `catchup.py`'s `_emit`) — no digest round-trip needed.
|
|
28
24
|
|
|
29
25
|
The ledger *is* still the right channel for the one case that genuinely needs it: a conflict
|
|
30
26
|
discovered by Task 13's hourly bundled `ccsched` job, with no session open to print anything live.
|
|
@@ -35,12 +35,12 @@ from datetime import datetime, timezone
|
|
|
35
35
|
from pathlib import Path
|
|
36
36
|
|
|
37
37
|
from cc_session_tools.lib import db as _db
|
|
38
|
+
from cc_session_tools.lib import paths as _paths
|
|
38
39
|
from cc_session_tools.lib.messaging.store import store_root as _default_messages_root
|
|
39
40
|
from cc_session_tools.lib.scheduler.state import parse_ts_or_none as _parse_ts_or_none
|
|
40
41
|
from cc_session_tools.lib.scheduler.store import scheduler_dir as _default_scheduler_dir
|
|
41
42
|
from cc_session_tools.lib.sessions_db import default_db_path as _default_sessions_db_path
|
|
42
43
|
|
|
43
|
-
DEFAULT_PROJECTS_DIR = Path.home() / ".claude" / "projects"
|
|
44
44
|
DEFAULT_SESSION_ENV_DIR = Path.home() / ".claude" / "session-env"
|
|
45
45
|
DEFAULT_MIN_AGE_HOURS = 24.0
|
|
46
46
|
|
|
@@ -217,7 +217,7 @@ def build_report(
|
|
|
217
217
|
override, e.g. ``CC_SCHEDULER_DIR`` / ``CCST_MESSAGES_ROOT`` /
|
|
218
218
|
``CCST_SESSIONS_DIR``).
|
|
219
219
|
"""
|
|
220
|
-
projects_dir = projects_dir if projects_dir is not None else
|
|
220
|
+
projects_dir = projects_dir if projects_dir is not None else _paths.claude_projects_dir()
|
|
221
221
|
scheduler_dir = scheduler_dir if scheduler_dir is not None else _default_scheduler_dir()
|
|
222
222
|
messages_root = messages_root if messages_root is not None else _default_messages_root()
|
|
223
223
|
session_env_dir = (
|
|
@@ -488,7 +488,7 @@ def prune(
|
|
|
488
488
|
``only``, when given, restricts processing to that subset of
|
|
489
489
|
``STORE_NAMES``.
|
|
490
490
|
"""
|
|
491
|
-
projects_dir = projects_dir if projects_dir is not None else
|
|
491
|
+
projects_dir = projects_dir if projects_dir is not None else _paths.claude_projects_dir()
|
|
492
492
|
scheduler_dir = scheduler_dir if scheduler_dir is not None else _default_scheduler_dir()
|
|
493
493
|
messages_root = messages_root if messages_root is not None else _default_messages_root()
|
|
494
494
|
session_env_dir = (
|
|
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Iterator
|
|
10
10
|
|
|
11
|
+
from cc_session_tools.lib import paths as _paths
|
|
11
12
|
from cc_session_tools.lib.rules import encode_cwd
|
|
12
13
|
|
|
13
14
|
SESSION_BASENAME_RE = re.compile(r"^(\d{8})(?:-to-\d{8})?-")
|
|
@@ -316,7 +317,7 @@ def transcript_dir_for_project(project_dir: Path) -> Path:
|
|
|
316
317
|
Encoding: each '/' and '.' in the absolute project path is replaced with '-'.
|
|
317
318
|
Does not check whether the directory exists.
|
|
318
319
|
"""
|
|
319
|
-
return
|
|
320
|
+
return _paths.claude_projects_dir() / encode_cwd(str(project_dir))
|
|
320
321
|
|
|
321
322
|
|
|
322
323
|
def _jsonl_has_custom_title(jsonl: Path, basename: str, suffix: str) -> bool:
|