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.
Files changed (318) hide show
  1. {cc_session_tools-4.0.2/src/cc_session_tools.egg-info → cc_session_tools-4.1.0}/PKG-INFO +6 -5
  2. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/README.md +5 -4
  3. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/pyproject.toml +1 -1
  4. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccst.py +117 -1
  5. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/config/hooks-bundle.json +0 -6
  6. cc_session_tools-4.1.0/src/cc_session_tools/lib/command_recovery.py +299 -0
  7. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/hook_registry.py +0 -2
  8. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/paths.py +7 -0
  9. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/sync_notify.py +7 -11
  10. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/session_gc.py +3 -3
  11. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/sessions.py +2 -1
  12. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/move-session/SKILL.md +30 -29
  13. {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
  14. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/update-command-cache/SKILL.md +29 -12
  15. {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
  16. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0/src/cc_session_tools.egg-info}/PKG-INFO +6 -5
  17. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/SOURCES.txt +4 -2
  18. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_doctor.py +1 -1
  19. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hook_dispatcher.py +6 -1
  20. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hooks_install_zero_arg.py +0 -1
  21. cc_session_tools-4.1.0/tests/test_ccst_move_session_cli.py +122 -0
  22. cc_session_tools-4.1.0/tests/test_ccst_telemetry_recover.py +214 -0
  23. cc_session_tools-4.1.0/tests/test_command_recovery.py +490 -0
  24. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_hooks_install.py +9 -0
  25. cc_session_tools-4.0.2/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -206
  26. cc_session_tools-4.0.2/src/hooks/pending_rename.py +0 -37
  27. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/LICENSE +0 -0
  28. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/setup.cfg +0 -0
  29. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/__init__.py +0 -0
  30. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/__init__.py +0 -0
  31. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccd.py +0 -0
  32. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccmsg.py +0 -0
  33. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccr.py +0 -0
  34. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccs.py +0 -0
  35. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/ccsched.py +0 -0
  36. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
  37. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
  38. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
  39. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
  40. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/config/api-keys.example +0 -0
  41. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/config/session-tag-hook.json +0 -0
  42. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/hooks_install.py +0 -0
  43. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/__init__.py +0 -0
  44. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/api_keys.py +0 -0
  45. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/bun_tools.py +0 -0
  46. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/claude_flags.py +0 -0
  47. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/claude_md_install.py +0 -0
  48. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/claude_md_store.py +0 -0
  49. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/context_overrides.py +0 -0
  50. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/db.py +0 -0
  51. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/debug.py +0 -0
  52. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/doctor.py +0 -0
  53. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
  54. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/install_sync.py +0 -0
  55. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/levenshtein.py +0 -0
  56. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/machine_identity.py +0 -0
  57. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
  58. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
  59. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
  60. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/lock.py +0 -0
  61. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/message.py +0 -0
  62. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
  63. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/repository.py +0 -0
  64. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/retention.py +0 -0
  65. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/service.py +0 -0
  66. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/session_ref.py +0 -0
  67. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/messaging/store.py +0 -0
  68. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/occupancy.py +0 -0
  69. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
  70. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/backup.py +0 -0
  71. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/classify.py +0 -0
  72. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
  73. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/dump.py +0 -0
  74. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/formats.py +0 -0
  75. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
  76. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/ids.py +0 -0
  77. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/importers.py +0 -0
  78. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
  79. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/init_service.py +0 -0
  80. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
  81. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/migrate_uuid.py +0 -0
  82. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/naming.py +0 -0
  83. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/project_claude_md.py +0 -0
  84. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/readiness.py +0 -0
  85. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/rehydrate.py +0 -0
  86. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/rename_group.py +0 -0
  87. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
  88. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/repository.py +0 -0
  89. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/resolve.py +0 -0
  90. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/service.py +0 -0
  91. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
  92. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/store.py +0 -0
  93. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/sync_check.py +0 -0
  94. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/sync_lock.py +0 -0
  95. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/vector_clock.py +0 -0
  96. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/vector_clock_store.py +0 -0
  97. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/verify.py +0 -0
  98. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
  99. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/picker.py +0 -0
  100. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/proc_lock.py +0 -0
  101. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/prompts.py +0 -0
  102. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/roots.py +0 -0
  103. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/rules.py +0 -0
  104. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
  105. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
  106. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
  107. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
  108. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/digest.py +0 -0
  109. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/due.py +0 -0
  110. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
  111. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
  112. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/ledger.py +0 -0
  113. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
  114. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
  115. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
  116. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
  117. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
  118. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/state.py +0 -0
  119. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/store.py +0 -0
  120. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/surface.py +0 -0
  121. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
  122. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/scheduler/worker.py +0 -0
  123. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/sessions_db.py +0 -0
  124. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/sessions_repair.py +0 -0
  125. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/shell_install.py +0 -0
  126. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/tasklist.py +0 -0
  127. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/telemetry_store.py +0 -0
  128. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/text_wrap.py +0 -0
  129. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/lib/timefmt.py +0 -0
  130. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +0 -0
  131. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/clean-hook-sessions/SKILL.md +0 -0
  132. {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
  133. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/compact-smart/SKILL.md +0 -0
  134. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
  135. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
  136. {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
  137. {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
  138. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/extract-pptx/SKILL.md +0 -0
  139. {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
  140. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/extract-to-text/SKILL.md +0 -0
  141. {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
  142. {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
  143. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
  144. {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
  145. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
  146. {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
  147. {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
  148. {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
  149. {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
  150. {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
  151. {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
  152. {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
  153. {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
  154. {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
  155. {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
  156. {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
  157. {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
  158. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
  159. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
  160. {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
  161. {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
  162. {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
  163. {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
  164. {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
  165. {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
  166. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
  167. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
  168. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
  169. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
  170. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/requires.txt +0 -0
  171. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/cc_session_tools.egg-info/top_level.txt +0 -0
  172. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/__init__.py +0 -0
  173. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/attribution.py +0 -0
  174. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/cache.py +0 -0
  175. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/ccusage_wrapper.py +0 -0
  176. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/cli.py +0 -0
  177. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/data/pricing.json +0 -0
  178. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/parent_inference.py +0 -0
  179. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/parser.py +0 -0
  180. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/pricing.py +0 -0
  181. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/query.py +0 -0
  182. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/report.py +0 -0
  183. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/schema.py +0 -0
  184. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/claude_code_usage/session_names.py +0 -0
  185. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/__init__.py +0 -0
  186. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/after_response.py +0 -0
  187. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/bash_hard_deny.py +0 -0
  188. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/bash_security_review.py +0 -0
  189. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/cache.py +0 -0
  190. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/catchup.py +0 -0
  191. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/confirm_8digit.py +0 -0
  192. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/context_window_warning.py +0 -0
  193. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/last_screenshot.py +0 -0
  194. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/marker_allow.py +0 -0
  195. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/markers.py +0 -0
  196. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/messaging_deliver.py +0 -0
  197. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/model_info.py +0 -0
  198. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/normalise.py +0 -0
  199. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/pdata_sync.py +0 -0
  200. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/pending_migration.py +0 -0
  201. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/script_allowlist.py +0 -0
  202. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/session_tag.py +0 -0
  203. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/stats.py +0 -0
  204. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/telemetry.py +0 -0
  205. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/telemetry_query.py +0 -0
  206. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/telemetry_trim.py +0 -0
  207. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/transcript.py +0 -0
  208. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/src/hooks/worklog_guard.py +0 -0
  209. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_after_response.py +0 -0
  210. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_api_keys.py +0 -0
  211. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_attribution.py +0 -0
  212. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_bash_hard_deny.py +0 -0
  213. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_bash_security_review.py +0 -0
  214. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_bash_security_review_allowlist.py +0 -0
  215. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_bun_tools.py +0 -0
  216. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cache.py +0 -0
  217. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cache_sqlite.py +0 -0
  218. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccr_orphans.py +0 -0
  219. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_emptiness.py +0 -0
  220. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_eta.py +0 -0
  221. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_list_mode.py +0 -0
  222. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_scope_flags.py +0 -0
  223. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_sentinel_sort.py +0 -0
  224. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccs_session_counts.py +0 -0
  225. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_bundle_discovery.py +0 -0
  226. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
  227. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_cli.py +0 -0
  228. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_context_override_cli.py +0 -0
  229. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_gc_prune.py +0 -0
  230. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_gc_report.py +0 -0
  231. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hooks_allowlist.py +0 -0
  232. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hooks_install_prune.py +0 -0
  233. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_hooks_uninstall.py +0 -0
  234. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_install_everything.py +0 -0
  235. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_machine_identity_cli.py +0 -0
  236. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_main_gate.py +0 -0
  237. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_cli.py +0 -0
  238. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_init_cli.py +0 -0
  239. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_migrate_uuid_cli.py +0 -0
  240. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_readiness_cli.py +0 -0
  241. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
  242. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_rename_group_cli.py +0 -0
  243. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
  244. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_sync_cli.py +0 -0
  245. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_pdata_verify_cli.py +0 -0
  246. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_repair_sessions_cli.py +0 -0
  247. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_sessions_cli.py +0 -0
  248. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_shell_install.py +0 -0
  249. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_skills_install.py +0 -0
  250. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_skills_uninstall.py +0 -0
  251. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_telemetry_query.py +0 -0
  252. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccst_telemetry_trim.py +0 -0
  253. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_ccusage_wrapper.py +0 -0
  254. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_claude_flags.py +0 -0
  255. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_claude_md_drift.py +0 -0
  256. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_claude_md_install.py +0 -0
  257. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_clean_hook_sessions_format.py +0 -0
  258. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli.py +0 -0
  259. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_api_launchers.py +0 -0
  260. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_ccd.py +0 -0
  261. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_ccd_dry_run.py +0 -0
  262. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_ccr.py +0 -0
  263. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_ccs.py +0 -0
  264. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_cli_version.py +0 -0
  265. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_confirm_8digit.py +0 -0
  266. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_context_overrides.py +0 -0
  267. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_context_window_warning.py +0 -0
  268. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_date_format_conformance.py +0 -0
  269. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_db.py +0 -0
  270. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_debug.py +0 -0
  271. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_doctor_mutes.py +0 -0
  272. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_empty_session.py +0 -0
  273. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_hook_cache.py +0 -0
  274. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_hooks_install_records.py +0 -0
  275. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_install_everything_migration_banner.py +0 -0
  276. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_install_sync.py +0 -0
  277. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_last_screenshot.py +0 -0
  278. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_levenshtein.py +0 -0
  279. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_machine_identity.py +0 -0
  280. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_marker_allow.py +0 -0
  281. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_migrate_ccmsg.py +0 -0
  282. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_migrate_ccsched.py +0 -0
  283. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_migrate_sessions_db.py +0 -0
  284. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_migrate_telemetry.py +0 -0
  285. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_model_info.py +0 -0
  286. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_normalise.py +0 -0
  287. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_occupancy.py +0 -0
  288. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_parent_inference.py +0 -0
  289. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_parser.py +0 -0
  290. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_paths.py +0 -0
  291. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pdata_audit_skill.py +0 -0
  292. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pdata_post_migration_skills.py +0 -0
  293. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pdata_uuid_migration_runbook.py +0 -0
  294. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pending_migration_hook.py +0 -0
  295. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_picker.py +0 -0
  296. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_pricing.py +0 -0
  297. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_proc_lock.py +0 -0
  298. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_prompts.py +0 -0
  299. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_query.py +0 -0
  300. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_report.py +0 -0
  301. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_roots.py +0 -0
  302. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_rules.py +0 -0
  303. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_scheduler_bundled_jobs.py +0 -0
  304. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_schema.py +0 -0
  305. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_script_allowlist.py +0 -0
  306. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_session_names.py +0 -0
  307. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_session_tag.py +0 -0
  308. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_sessions.py +0 -0
  309. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_sessions_db.py +0 -0
  310. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_sessions_db_migration_chain.py +0 -0
  311. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_sessions_repair.py +0 -0
  312. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_tasklist.py +0 -0
  313. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_telemetry.py +0 -0
  314. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_telemetry_store.py +0 -0
  315. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_text_wrap.py +0 -0
  316. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_timefmt.py +0 -0
  317. {cc_session_tools-4.0.2 → cc_session_tools-4.1.0}/tests/test_transcript.py +0 -0
  318. {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.2
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` and `pending-rename` - surface unfinished data migrations and
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-invocation log |
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. `ccst doctor` is the one
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` and `pending-rename` - surface unfinished data migrations and
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-invocation log |
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. `ccst doctor` is the one
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.2"
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":
@@ -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"
@@ -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 reuse the other SessionStart mechanism this repo has —
18
- `hooks.pending_rename`'s dedicated `.pending-rename`-marker bash hook. That mechanism is not
19
- a general-purpose "digest queue" a second caller can feed; it is a bespoke marker file plus a
20
- bespoke bash script checking exactly that one file, with no reusable write-side API at all —
21
- using it here would mean building a third channel (a new marker format, a new SessionStart hook
22
- script), which is exactly the "not building either channel" scope this module is meant to stay
23
- inside. It would also be redundant for the case it might otherwise justify itself for: a live
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 DEFAULT_PROJECTS_DIR
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 DEFAULT_PROJECTS_DIR
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 Path.home() / ".claude" / "projects" / encode_cwd(str(project_dir))
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: