cc-session-tools 3.6.1__tar.gz → 3.7.1__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 (291) hide show
  1. {cc_session_tools-3.6.1/src/cc_session_tools.egg-info → cc_session_tools-3.7.1}/PKG-INFO +3 -3
  2. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/README.md +2 -2
  3. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/pyproject.toml +1 -1
  4. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/ccst.py +114 -17
  5. cc_session_tools-3.7.1/src/cc_session_tools/lib/bun_tools.py +39 -0
  6. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/doctor.py +79 -6
  7. cc_session_tools-3.7.1/src/cc_session_tools/lib/text_wrap.py +35 -0
  8. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +22 -2
  9. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1/src/cc_session_tools.egg-info}/PKG-INFO +3 -3
  10. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools.egg-info/SOURCES.txt +9 -0
  11. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/ccusage_wrapper.py +12 -6
  12. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/bash_security_review.py +166 -13
  13. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/cache.py +94 -0
  14. cc_session_tools-3.7.1/src/hooks/script_allowlist.py +145 -0
  15. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_bash_security_review.py +78 -4
  16. cc_session_tools-3.7.1/tests/test_bash_security_review_allowlist.py +330 -0
  17. cc_session_tools-3.7.1/tests/test_bun_tools.py +67 -0
  18. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_doctor.py +124 -0
  19. cc_session_tools-3.7.1/tests/test_ccst_hooks_allowlist.py +114 -0
  20. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_hooks_install_zero_arg.py +47 -11
  21. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccusage_wrapper.py +55 -0
  22. cc_session_tools-3.7.1/tests/test_hooks_install_records.py +104 -0
  23. cc_session_tools-3.7.1/tests/test_script_allowlist.py +288 -0
  24. cc_session_tools-3.7.1/tests/test_text_wrap.py +70 -0
  25. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/LICENSE +0 -0
  26. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/setup.cfg +0 -0
  27. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/__init__.py +0 -0
  28. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/__init__.py +0 -0
  29. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/ccd.py +0 -0
  30. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/ccmsg.py +0 -0
  31. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/ccr.py +0 -0
  32. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/ccs.py +0 -0
  33. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/ccsched.py +0 -0
  34. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
  35. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
  36. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
  37. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
  38. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/config/hooks-bundle.json +0 -0
  39. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/config/session-tag-hook.json +0 -0
  40. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/hooks_install.py +0 -0
  41. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/__init__.py +0 -0
  42. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/claude_flags.py +0 -0
  43. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/claude_md_install.py +0 -0
  44. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/context_overrides.py +0 -0
  45. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/db.py +0 -0
  46. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/debug.py +0 -0
  47. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
  48. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/hook_registry.py +0 -0
  49. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/install_sync.py +0 -0
  50. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/levenshtein.py +0 -0
  51. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/machine_identity.py +0 -0
  52. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
  53. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
  54. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
  55. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/lock.py +0 -0
  56. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/message.py +0 -0
  57. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
  58. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/repository.py +0 -0
  59. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/retention.py +0 -0
  60. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/service.py +0 -0
  61. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/messaging/store.py +0 -0
  62. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/occupancy.py +0 -0
  63. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/paths.py +0 -0
  64. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
  65. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/backup.py +0 -0
  66. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/classify.py +0 -0
  67. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
  68. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/dump.py +0 -0
  69. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
  70. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/importers.py +0 -0
  71. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
  72. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/init_service.py +0 -0
  73. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
  74. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/naming.py +0 -0
  75. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/rehydrate.py +0 -0
  76. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/rename_group.py +0 -0
  77. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
  78. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/repository.py +0 -0
  79. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/resolve.py +0 -0
  80. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/service.py +0 -0
  81. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
  82. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/store.py +0 -0
  83. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/sync_check.py +0 -0
  84. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/sync_lock.py +0 -0
  85. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/sync_notify.py +0 -0
  86. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/vector_clock.py +0 -0
  87. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/vector_clock_store.py +0 -0
  88. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/verify.py +0 -0
  89. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
  90. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/picker.py +0 -0
  91. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/proc_lock.py +0 -0
  92. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/prompts.py +0 -0
  93. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/roots.py +0 -0
  94. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/rules.py +0 -0
  95. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
  96. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
  97. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
  98. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
  99. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/digest.py +0 -0
  100. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/due.py +0 -0
  101. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
  102. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
  103. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/ledger.py +0 -0
  104. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
  105. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
  106. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
  107. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
  108. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
  109. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/state.py +0 -0
  110. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/store.py +0 -0
  111. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/surface.py +0 -0
  112. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
  113. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/scheduler/worker.py +0 -0
  114. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/session_gc.py +0 -0
  115. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/sessions.py +0 -0
  116. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/sessions_db.py +0 -0
  117. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/sessions_repair.py +0 -0
  118. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/shell_install.py +0 -0
  119. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/tasklist.py +0 -0
  120. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/lib/telemetry_store.py +0 -0
  121. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
  122. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
  123. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/clean-hook-sessions/SKILL.md +0 -0
  124. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/clean-hook-sessions/scripts/clean-hook-sessions.py +0 -0
  125. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/compact-smart/SKILL.md +0 -0
  126. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
  127. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
  128. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
  129. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
  130. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/extract-pptx/SKILL.md +0 -0
  131. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/extract-pptx/scripts/extract_pptx.py +0 -0
  132. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/extract-to-text/SKILL.md +0 -0
  133. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/extract-to-text/scripts/extract_to_text.py +0 -0
  134. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
  135. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
  136. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
  137. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
  138. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
  139. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
  140. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
  141. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
  142. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
  143. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
  144. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/pm-pdata-design-schema/SKILL.md +0 -0
  145. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/pm-pdata-do-audit-and-prepare-to-migrate/SKILL.md +0 -0
  146. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/pm-pdata-do-init/SKILL.md +0 -0
  147. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/pm-pdata-do-manual-migration/SKILL.md +0 -0
  148. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/pm-pdata-resolve-conflicts/SKILL.md +0 -0
  149. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
  150. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
  151. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
  152. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
  153. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
  154. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
  155. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
  156. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
  157. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
  158. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
  159. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
  160. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
  161. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/update-command-cache/SKILL.md +0 -0
  162. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +0 -0
  163. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
  164. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
  165. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools.egg-info/requires.txt +0 -0
  166. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/cc_session_tools.egg-info/top_level.txt +0 -0
  167. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/__init__.py +0 -0
  168. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/attribution.py +0 -0
  169. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/cache.py +0 -0
  170. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/cli.py +0 -0
  171. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/data/pricing.json +0 -0
  172. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/parent_inference.py +0 -0
  173. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/parser.py +0 -0
  174. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/pricing.py +0 -0
  175. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/query.py +0 -0
  176. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/report.py +0 -0
  177. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/schema.py +0 -0
  178. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/claude_code_usage/session_names.py +0 -0
  179. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/__init__.py +0 -0
  180. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/after_response.py +0 -0
  181. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/bash_hard_deny.py +0 -0
  182. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/catchup.py +0 -0
  183. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/confirm_8digit.py +0 -0
  184. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/context_window_warning.py +0 -0
  185. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/last_screenshot.py +0 -0
  186. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/marker_allow.py +0 -0
  187. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/markers.py +0 -0
  188. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/messaging_deliver.py +0 -0
  189. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/model_info.py +0 -0
  190. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/normalise.py +0 -0
  191. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/pdata_sync.py +0 -0
  192. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/pending_migration.py +0 -0
  193. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/pending_rename.py +0 -0
  194. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/session_tag.py +0 -0
  195. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/stats.py +0 -0
  196. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/telemetry.py +0 -0
  197. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/telemetry_query.py +0 -0
  198. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/telemetry_trim.py +0 -0
  199. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/transcript.py +0 -0
  200. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/src/hooks/worklog_guard.py +0 -0
  201. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_after_response.py +0 -0
  202. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_attribution.py +0 -0
  203. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_bash_hard_deny.py +0 -0
  204. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_cache.py +0 -0
  205. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_cache_sqlite.py +0 -0
  206. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccr_orphans.py +0 -0
  207. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccs_emptiness.py +0 -0
  208. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccs_eta.py +0 -0
  209. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccs_list_mode.py +0 -0
  210. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccs_scope_flags.py +0 -0
  211. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccs_sentinel_sort.py +0 -0
  212. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccs_session_counts.py +0 -0
  213. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_bundle_discovery.py +0 -0
  214. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
  215. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_cli.py +0 -0
  216. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_context_override_cli.py +0 -0
  217. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_gc_prune.py +0 -0
  218. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_gc_report.py +0 -0
  219. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_hook_dispatcher.py +0 -0
  220. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_hooks_install_prune.py +0 -0
  221. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_hooks_uninstall.py +0 -0
  222. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_install_everything.py +0 -0
  223. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_machine_identity_cli.py +0 -0
  224. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_main_gate.py +0 -0
  225. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_pdata_cli.py +0 -0
  226. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_pdata_init_cli.py +0 -0
  227. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
  228. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_pdata_rename_group_cli.py +0 -0
  229. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
  230. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_pdata_sync_cli.py +0 -0
  231. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_pdata_verify_cli.py +0 -0
  232. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_repair_sessions_cli.py +0 -0
  233. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_sessions_cli.py +0 -0
  234. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_shell_install.py +0 -0
  235. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_skills_install.py +0 -0
  236. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_skills_uninstall.py +0 -0
  237. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_telemetry_query.py +0 -0
  238. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_ccst_telemetry_trim.py +0 -0
  239. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_claude_flags.py +0 -0
  240. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_claude_md_install.py +0 -0
  241. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_cli.py +0 -0
  242. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_cli_ccd.py +0 -0
  243. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_cli_ccd_dry_run.py +0 -0
  244. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_cli_ccr.py +0 -0
  245. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_cli_ccs.py +0 -0
  246. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_cli_version.py +0 -0
  247. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_confirm_8digit.py +0 -0
  248. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_context_overrides.py +0 -0
  249. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_context_window_warning.py +0 -0
  250. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_db.py +0 -0
  251. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_debug.py +0 -0
  252. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_doctor_mutes.py +0 -0
  253. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_empty_session.py +0 -0
  254. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_hook_cache.py +0 -0
  255. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_hooks_install.py +0 -0
  256. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_install_sync.py +0 -0
  257. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_last_screenshot.py +0 -0
  258. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_levenshtein.py +0 -0
  259. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_machine_identity.py +0 -0
  260. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_marker_allow.py +0 -0
  261. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_migrate_ccmsg.py +0 -0
  262. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_migrate_ccsched.py +0 -0
  263. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_migrate_sessions_db.py +0 -0
  264. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_migrate_telemetry.py +0 -0
  265. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_model_info.py +0 -0
  266. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_normalise.py +0 -0
  267. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_occupancy.py +0 -0
  268. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_parent_inference.py +0 -0
  269. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_parser.py +0 -0
  270. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_paths.py +0 -0
  271. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_pending_migration_hook.py +0 -0
  272. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_picker.py +0 -0
  273. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_pricing.py +0 -0
  274. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_proc_lock.py +0 -0
  275. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_prompts.py +0 -0
  276. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_query.py +0 -0
  277. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_report.py +0 -0
  278. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_roots.py +0 -0
  279. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_rules.py +0 -0
  280. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_scheduler_bundled_jobs.py +0 -0
  281. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_schema.py +0 -0
  282. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_session_names.py +0 -0
  283. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_session_tag.py +0 -0
  284. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_sessions.py +0 -0
  285. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_sessions_db.py +0 -0
  286. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_sessions_repair.py +0 -0
  287. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_tasklist.py +0 -0
  288. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_telemetry.py +0 -0
  289. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_telemetry_store.py +0 -0
  290. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/tests/test_transcript.py +0 -0
  291. {cc_session_tools-3.6.1 → cc_session_tools-3.7.1}/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: 3.6.1
3
+ Version: 3.7.1
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
@@ -105,7 +105,7 @@ bundled scheduled jobs:
105
105
  | **`session-tag`** (SessionStart) | Writes a `<uuid>.tag` file so `claude-code-usage` can map session UUIDs to human-readable names. |
106
106
  | **`last-screenshot`** (UserPromptSubmit) | Resolves your newest screenshot for the `>lss` token and injects its path. Requires `CCST_SCREENSHOT_DIR`. |
107
107
  | **`bash-hard-deny`** (PreToolUse) | Categorical hard-deny gate for Bash: destructive file ops (`rm`/`rmdir`/`unlink`/`shred`, incl. inline python/node and script-file/heredoc forms), delete-by-move to a tmp-like location, `git branch`/`git push` branch deletion, `gh api`/`gh release` delete, destructive curl/wget methods, `sudo`, and direct telemetry-log reads. Runs before every other Bash hook; a match here can never be overridden in-session (the message tells you to run the command yourself in a separate terminal). |
108
- | **`bash-security-review`** (PreToolUse) | Tiered Bash command security review with an allowlist cache and LLM fallback. |
108
+ | **`bash-security-review`** (PreToolUse) | Tiered Bash command security review: trivial/read-only pre-filters, heuristic escalation, a reviewed-script allowlist (script path + content hash, managed with `ccst hooks allowlist list\|add\|remove\|verify`), a command cache, and a slimmed `claude -p` LLM fallback (~770 tokens per review instead of ~50k+). |
109
109
  | **`marker-allow`** (PreToolUse) | Auto-approves a bare `touch` of a skill marker under `~/.cache/claude/markers/` (and nothing else), so marker-gated skills can refresh their TTL marker without a permission prompt. |
110
110
  | **`confirm-8digit`** (PreToolUse) | Blocks a configurable set of high-stakes tool calls unless the user repeats back an 8-digit confirmation code. |
111
111
  | **`after-response`** (Stop) | Touches a `.last-active` sentinel so `ccs --order-by active` can sort sessions by recency of Claude activity. |
@@ -151,7 +151,7 @@ If you've ever tried to remember which `1f4a8b3c-...` UUID is the session where
151
151
 
152
152
  - **Python 3.11+** (3.12+ recommended)
153
153
  - **The `claude` CLI on your `$PATH`.** Install it first via [the official Claude Code instructions](https://docs.anthropic.com/en/docs/claude-code/setup) and verify with `claude --version`.
154
- - **`ccusage` (optional)** - if on `$PATH`, `claude-code-usage reconcile` cross-checks dollar totals against it. Skipped gracefully if missing.
154
+ - **`ccusage` (optional)** - if on `$PATH` (or in bun's global bin dir, `~/.bun/bin`), `claude-code-usage reconcile` cross-checks dollar totals against it. Skipped gracefully if missing. Needs `bun` to install; `ccst doctor` reports both under `deps:bun` / `deps:ccusage` and prints the install command. See the `analyse-cc-usage` skill's setup section.
155
155
  - **`ripgrep` (optional)** - `ccs --contents` prefers `rg`; falls back to threaded Python `grep` if missing.
156
156
 
157
157
  ### Install and set up
@@ -48,7 +48,7 @@ bundled scheduled jobs:
48
48
  | **`session-tag`** (SessionStart) | Writes a `<uuid>.tag` file so `claude-code-usage` can map session UUIDs to human-readable names. |
49
49
  | **`last-screenshot`** (UserPromptSubmit) | Resolves your newest screenshot for the `>lss` token and injects its path. Requires `CCST_SCREENSHOT_DIR`. |
50
50
  | **`bash-hard-deny`** (PreToolUse) | Categorical hard-deny gate for Bash: destructive file ops (`rm`/`rmdir`/`unlink`/`shred`, incl. inline python/node and script-file/heredoc forms), delete-by-move to a tmp-like location, `git branch`/`git push` branch deletion, `gh api`/`gh release` delete, destructive curl/wget methods, `sudo`, and direct telemetry-log reads. Runs before every other Bash hook; a match here can never be overridden in-session (the message tells you to run the command yourself in a separate terminal). |
51
- | **`bash-security-review`** (PreToolUse) | Tiered Bash command security review with an allowlist cache and LLM fallback. |
51
+ | **`bash-security-review`** (PreToolUse) | Tiered Bash command security review: trivial/read-only pre-filters, heuristic escalation, a reviewed-script allowlist (script path + content hash, managed with `ccst hooks allowlist list\|add\|remove\|verify`), a command cache, and a slimmed `claude -p` LLM fallback (~770 tokens per review instead of ~50k+). |
52
52
  | **`marker-allow`** (PreToolUse) | Auto-approves a bare `touch` of a skill marker under `~/.cache/claude/markers/` (and nothing else), so marker-gated skills can refresh their TTL marker without a permission prompt. |
53
53
  | **`confirm-8digit`** (PreToolUse) | Blocks a configurable set of high-stakes tool calls unless the user repeats back an 8-digit confirmation code. |
54
54
  | **`after-response`** (Stop) | Touches a `.last-active` sentinel so `ccs --order-by active` can sort sessions by recency of Claude activity. |
@@ -94,7 +94,7 @@ If you've ever tried to remember which `1f4a8b3c-...` UUID is the session where
94
94
 
95
95
  - **Python 3.11+** (3.12+ recommended)
96
96
  - **The `claude` CLI on your `$PATH`.** Install it first via [the official Claude Code instructions](https://docs.anthropic.com/en/docs/claude-code/setup) and verify with `claude --version`.
97
- - **`ccusage` (optional)** - if on `$PATH`, `claude-code-usage reconcile` cross-checks dollar totals against it. Skipped gracefully if missing.
97
+ - **`ccusage` (optional)** - if on `$PATH` (or in bun's global bin dir, `~/.bun/bin`), `claude-code-usage reconcile` cross-checks dollar totals against it. Skipped gracefully if missing. Needs `bun` to install; `ccst doctor` reports both under `deps:bun` / `deps:ccusage` and prints the install command. See the `analyse-cc-usage` skill's setup section.
98
98
  - **`ripgrep` (optional)** - `ccs --contents` prefers `rg`; falls back to threaded Python `grep` if missing.
99
99
 
100
100
  ### Install and set up
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cc-session-tools"
7
- version = "3.6.1"
7
+ version = "3.7.1"
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" }
@@ -6,6 +6,8 @@ Current subcommands:
6
6
  hooks install [--hook <name>] Merge hook entries from the bundled set (or a
7
7
  custom --source) into a target settings.json.
8
8
  hooks uninstall [--hook <name>] Remove hook entries from a target settings.json.
9
+ hooks allowlist list|add|remove|verify
10
+ Manage the bash-security-review reviewed-script allowlist.
9
11
  hooks run <name> Run a Claude Code hook by name.
10
12
  Available hooks: bash-hard-deny,
11
13
  bash-security-review, marker-allow,
@@ -110,6 +112,7 @@ from cc_session_tools.hooks_install import (
110
112
  )
111
113
  from cc_session_tools.lib import machine_identity
112
114
  from cc_session_tools.lib.hook_registry import HOOK_DESCRIPTIONS, HOOK_VERBS
115
+ from cc_session_tools.lib.text_wrap import wrap_block
113
116
 
114
117
  if TYPE_CHECKING:
115
118
  # Type-only, so the handler's own lazy `from ... import sync_check` (this file's convention:
@@ -536,30 +539,45 @@ def _bundle_inventory(bundle: dict[str, Any]) -> list[tuple[str, str, str | None
536
539
  return out
537
540
 
538
541
 
539
- def _print_hooks_install_table(
542
+ def _format_hooks_install_records(
540
543
  inventory: list[tuple[str, str, str | None, str]],
541
544
  added_keys: set[tuple[str, str | None, str]],
542
- ) -> None:
543
- """Print a Hook | Status | Event | Description table to stdout."""
544
- headers = ("Hook", "Status", "Event", "Description")
545
- if not inventory:
546
- return
545
+ ) -> str:
546
+ """One record per hook: a `name status` header, an indented `Events:` line, then the
547
+ indented, terminal-width-wrapped description. Records are separated by a blank line.
547
548
 
548
- rows: list[tuple[str, str, str, str]] = []
549
+ A hook registered for several events is one record; when only some of its registrations
550
+ would be added, those events carry a `(new)` marker. Order follows the inventory (sorted by
551
+ hook name).
552
+ """
553
+ by_hook: dict[str, list[tuple[bool, str]]] = {}
549
554
  for name, event, matcher, cmd in inventory:
550
- status = "install" if (event, matcher, cmd) in added_keys else "already-installed"
551
555
  event_label = f"{event}[{matcher}]" if matcher else event
556
+ by_hook.setdefault(name, []).append(((event, matcher, cmd) in added_keys, event_label))
557
+
558
+ records: list[str] = []
559
+ for name, registrations in by_hook.items():
560
+ any_new = any(is_new for is_new, _ in registrations)
561
+ mixed = any_new and not all(is_new for is_new, _ in registrations)
562
+ status = "install" if any_new else "already-installed"
563
+ events = ", ".join(
564
+ f"{label} (new)" if mixed and is_new else label for is_new, label in registrations
565
+ )
566
+ lines = [f"{name} {status}", wrap_block(f"Events: {events}")]
552
567
  description = HOOK_DESCRIPTIONS.get(name, "")
553
- rows.append((name, status, event_label, description))
568
+ if description:
569
+ lines.append(wrap_block(description))
570
+ records.append("\n".join(lines))
571
+ return "\n\n".join(records)
554
572
 
555
- widths = [
556
- max([len(headers[i])] + [len(r[i]) for r in rows]) for i in range(4)
557
- ]
558
- fmt = f"{{:<{widths[0]}}} {{:<{widths[1]}}} {{:<{widths[2]}}} {{:<{widths[3]}}}"
559
- print(fmt.format(*headers))
560
- print(fmt.format(*("-" * w for w in widths)))
561
- for row in rows:
562
- print(fmt.format(*row))
573
+
574
+ def _print_hooks_install_table(
575
+ inventory: list[tuple[str, str, str | None, str]],
576
+ added_keys: set[tuple[str, str | None, str]],
577
+ ) -> None:
578
+ """Print the per-hook install records to stdout."""
579
+ if inventory:
580
+ print(_format_hooks_install_records(inventory, added_keys))
563
581
 
564
582
 
565
583
  def _list_bundle_hook_names(bundle: dict[str, Any]) -> list[str]:
@@ -1949,6 +1967,65 @@ def _cmd_machine_identity_confirm(args: argparse.Namespace) -> int:
1949
1967
  # ---------- hooks run ----------
1950
1968
 
1951
1969
 
1970
+ def _cmd_hooks_allowlist(args: argparse.Namespace) -> int:
1971
+ """`ccst hooks allowlist list|add|remove|verify` - the reviewed-script allowlist store."""
1972
+ import hashlib
1973
+
1974
+ from hooks import cache as cache_mod
1975
+ from hooks import script_allowlist
1976
+
1977
+ verb = args.allowlist_verb
1978
+ if verb == "list":
1979
+ entries = cache_mod.allowlist_list()
1980
+ if not entries:
1981
+ print("No allowlisted scripts.")
1982
+ return 0
1983
+ for e in entries:
1984
+ print(
1985
+ f"{e.script_path} source={e.source} sha256={e.sha256[:8]} added={e.added_at} "
1986
+ f"last_used={e.last_used or '-'} uses={e.use_count}"
1987
+ )
1988
+ return 0
1989
+ if verb == "add":
1990
+ path = Path(args.script).expanduser().resolve()
1991
+ if not path.is_file():
1992
+ print(f"error: {args.script}: not a file", file=sys.stderr)
1993
+ return 2
1994
+ root = script_allowlist.find_project_root(path.parent)
1995
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
1996
+ cache_mod.allowlist_upsert(
1997
+ str(path), digest, str(root), source="manual", overwrite_source=True
1998
+ )
1999
+ print(f"Allowlisted {path} (sha256 {digest[:8]}, project {root})")
2000
+ return 0
2001
+ if verb == "remove":
2002
+ path = Path(args.script).expanduser().resolve()
2003
+ if not cache_mod.allowlist_remove(str(path)):
2004
+ print(f"error: {path} is not in the allowlist", file=sys.stderr)
2005
+ return 1
2006
+ print(f"Removed {path}")
2007
+ return 0
2008
+ # verify
2009
+ drift = 0
2010
+ entries = cache_mod.allowlist_list()
2011
+ if not entries:
2012
+ print("No allowlisted scripts.")
2013
+ return 0
2014
+ for e in entries:
2015
+ p = Path(e.script_path)
2016
+ if not p.is_file():
2017
+ print(f"MISSING {e.script_path}")
2018
+ drift += 1
2019
+ continue
2020
+ current = hashlib.sha256(p.read_bytes()).hexdigest()
2021
+ if current != e.sha256:
2022
+ print(f"CHANGED {e.script_path} {e.sha256[:8]} -> {current[:8]}")
2023
+ drift += 1
2024
+ else:
2025
+ print(f"OK {e.script_path}")
2026
+ return 1 if drift else 0
2027
+
2028
+
1952
2029
  def _cmd_hooks_run(args: argparse.Namespace) -> int:
1953
2030
  """Dispatch to the named hook, or exit 1 (never 2) if it is unknown.
1954
2031
 
@@ -2598,6 +2675,24 @@ def _build_parser() -> argparse.ArgumentParser:
2598
2675
  help="Write changes (default: dry run)",
2599
2676
  )
2600
2677
 
2678
+ # hooks allowlist
2679
+ allowlist_parser = hooks_sub.add_parser(
2680
+ "allowlist",
2681
+ help="Manage the bash-security-review reviewed-script allowlist",
2682
+ )
2683
+ allowlist_sub = allowlist_parser.add_subparsers(dest="allowlist_verb", metavar="<action>")
2684
+ allowlist_sub.required = True
2685
+ allowlist_sub.add_parser("list", help="List allowlisted scripts")
2686
+ allowlist_add = allowlist_sub.add_parser(
2687
+ "add", help="Allowlist a script at its current content hash (you vouch for it)"
2688
+ )
2689
+ allowlist_add.add_argument("script", metavar="<script>", help="Path to the script")
2690
+ allowlist_remove = allowlist_sub.add_parser("remove", help="Remove a script from the allowlist")
2691
+ allowlist_remove.add_argument("script", metavar="<script>", help="Path to the script")
2692
+ allowlist_sub.add_parser(
2693
+ "verify", help="Report allowlisted scripts that changed or no longer exist (exit 1 on drift)"
2694
+ )
2695
+
2601
2696
  # hooks run
2602
2697
  run_parser = hooks_sub.add_parser(
2603
2698
  "run",
@@ -3500,6 +3595,8 @@ def main() -> None:
3500
3595
  sys.exit(_cmd_hooks_install(args))
3501
3596
  if args.verb == "uninstall":
3502
3597
  sys.exit(_cmd_hooks_uninstall(args))
3598
+ if args.verb == "allowlist":
3599
+ sys.exit(_cmd_hooks_allowlist(args))
3503
3600
  if args.verb == "run":
3504
3601
  sys.exit(_cmd_hooks_run(args))
3505
3602
 
@@ -0,0 +1,39 @@
1
+ """Locate tools installed through bun (bun itself, ccusage).
2
+
3
+ `bun add -g` installs into ``$BUN_INSTALL/bin`` (default ``~/.bun/bin``). That directory is
4
+ only on ``PATH`` if the user edited a shell rc file that the current shell type reads, which
5
+ non-interactive shells (hooks, scheduled jobs, subagent Bash calls) often do not. Resolving the
6
+ directory directly keeps a correct install findable from every shell type.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import dataclasses
11
+ import os
12
+ import shutil
13
+ from pathlib import Path
14
+
15
+ # Upstream's installer works on macOS and on Linux/WSL (which also needs `unzip` installed).
16
+ BUN_INSTALL_COMMAND = "curl -fsSL https://bun.sh/install | bash"
17
+ CCUSAGE_INSTALL_COMMAND = "bun add -g ccusage"
18
+
19
+
20
+ @dataclasses.dataclass(frozen=True, slots=True)
21
+ class Resolved:
22
+ path: str
23
+ on_path: bool
24
+
25
+
26
+ def bun_bin_dir() -> Path:
27
+ install = os.environ.get("BUN_INSTALL")
28
+ return (Path(install) if install else Path.home() / ".bun") / "bin"
29
+
30
+
31
+ def resolve(name: str, *, bin_dir: Path | None = None) -> Resolved | None:
32
+ """Find *name* on ``PATH`` first, then in bun's global bin directory."""
33
+ found = shutil.which(name)
34
+ if found is not None:
35
+ return Resolved(path=found, on_path=True)
36
+ candidate = (bin_dir if bin_dir is not None else bun_bin_dir()) / name
37
+ if candidate.is_file() and os.access(candidate, os.X_OK):
38
+ return Resolved(path=str(candidate), on_path=False)
39
+ return None
@@ -14,13 +14,13 @@ import os
14
14
  import shutil
15
15
  import sqlite3
16
16
  import subprocess
17
- import textwrap
18
17
  import time
19
18
  from dataclasses import dataclass
20
19
  from enum import Enum
21
20
  from pathlib import Path
22
21
  from typing import TYPE_CHECKING, Any
23
22
 
23
+ from cc_session_tools.lib import bun_tools
24
24
  from cc_session_tools.lib.db import connect as _db_connect
25
25
  from cc_session_tools.lib.db import migration_applied as _migration_applied
26
26
  from cc_session_tools.lib.hook_registry import HOOK_VERBS, hook_name_from_command
@@ -28,6 +28,7 @@ from cc_session_tools.lib.pdata.init_paths import (
28
28
  MIGRATED_ARCHIVE_DIRNAME,
29
29
  MIGRATED_MANIFEST_FILENAME,
30
30
  )
31
+ from cc_session_tools.lib.text_wrap import wrap_block
31
32
 
32
33
  if TYPE_CHECKING:
33
34
  from cc_session_tools.lib.install_sync import FailedAttempt
@@ -121,6 +122,69 @@ def check_env_dir(
121
122
  return CheckResult(name=name, status=Status.OK, reason=str(p))
122
123
 
123
124
 
125
+ def _check_bun_tool(
126
+ name: str, *, install_hint: str, bin_dir: Path | None, timeout: float
127
+ ) -> CheckResult:
128
+ """WARN-only check for an optional bun-installed tool (see `bun_tools.resolve`)."""
129
+ check_name = f"deps:{name}"
130
+ resolved = bun_tools.resolve(name, bin_dir=bin_dir)
131
+ if resolved is None:
132
+ return CheckResult(name=check_name, status=Status.WARN, reason=f"{name!r} not found; {install_hint}")
133
+ if not resolved.on_path:
134
+ found_dir = Path(resolved.path).parent
135
+ return CheckResult(
136
+ name=check_name,
137
+ status=Status.WARN,
138
+ reason=(
139
+ f"installed at {resolved.path} but {found_dir} is not on PATH; add it to PATH "
140
+ f"(for non-interactive shells too: ~/.zshenv for zsh, ~/.profile for bash)"
141
+ ),
142
+ )
143
+ try:
144
+ result = subprocess.run(
145
+ [resolved.path, "--version"], capture_output=True, text=True, timeout=timeout
146
+ )
147
+ except (OSError, subprocess.TimeoutExpired) as e:
148
+ return CheckResult(
149
+ name=check_name, status=Status.WARN, reason=f"could not run {name} --version: {e}"
150
+ )
151
+ if result.returncode != 0:
152
+ return CheckResult(
153
+ name=check_name, status=Status.WARN, reason=f"{name} --version exited {result.returncode}"
154
+ )
155
+ return CheckResult(
156
+ name=check_name,
157
+ status=Status.OK,
158
+ reason=(result.stdout + result.stderr).strip().split("\n")[0],
159
+ )
160
+
161
+
162
+ def check_bun(bin_dir: Path | None = None, timeout: float = 5.0) -> CheckResult:
163
+ """WARN (never FAIL) if bun, needed to install ccusage, is missing or not on PATH."""
164
+ return _check_bun_tool(
165
+ "bun",
166
+ install_hint=(
167
+ f"install with `{bun_tools.BUN_INSTALL_COMMAND}` (macOS and Linux/WSL; "
168
+ "Linux/WSL also needs `unzip`)"
169
+ ),
170
+ bin_dir=bin_dir,
171
+ timeout=timeout,
172
+ )
173
+
174
+
175
+ def check_ccusage(bin_dir: Path | None = None, timeout: float = 5.0) -> CheckResult:
176
+ """WARN (never FAIL) if ccusage, used by `claude-code-usage reconcile`, is missing.
177
+
178
+ ccusage is optional: claude-code-usage works without it apart from `reconcile`.
179
+ """
180
+ return _check_bun_tool(
181
+ "ccusage",
182
+ install_hint=f"install bun first (see deps:bun), then `{bun_tools.CCUSAGE_INSTALL_COMMAND}`",
183
+ bin_dir=bin_dir,
184
+ timeout=timeout,
185
+ )
186
+
187
+
124
188
  def check_settings_json(settings_path: Path) -> CheckResult:
125
189
  """Verify settings.json exists and is valid JSON."""
126
190
  name = "settings.json"
@@ -1111,6 +1175,10 @@ def run_all_checks(
1111
1175
  results.extend(check_sessions_uuid_migration(sessions_db_path))
1112
1176
  results.extend(check_sessions_project_dir_absolute(sessions_db_path))
1113
1177
 
1178
+ # Optional dependencies for claude-code-usage's reconcile / ccusage blocks
1179
+ results.append(check_bun())
1180
+ results.append(check_ccusage())
1181
+
1114
1182
  # PyPI version check
1115
1183
  if not skip_pypi:
1116
1184
  results.append(check_pypi_version(installed_version))
@@ -1156,12 +1224,11 @@ def _wrap_reason(reason: str, *, indent: str = " ") -> str:
1156
1224
  embedded paths and remediation commands); printed on a single line they
1157
1225
  hard-wrap at the terminal edge with no continuation indent, making a
1158
1226
  wrapped reason indistinguishable from a new, unrelated result. Wrapping
1159
- to the real terminal width — falling back to 80 columns for non-tty
1227
+ to the real terminal width - falling back to 80 columns for non-tty
1160
1228
  output (piped, redirected, or the scheduled drift-monitor job whose
1161
- stdout is forwarded to Telegram) — and indenting every line fixes that.
1229
+ stdout is forwarded to Telegram) - and indenting every line fixes that.
1162
1230
  """
1163
- width = shutil.get_terminal_size(fallback=(80, 24)).columns
1164
- return textwrap.fill(reason, width=width, initial_indent=indent, subsequent_indent=indent)
1231
+ return wrap_block(reason, indent=indent)
1165
1232
 
1166
1233
 
1167
1234
  def format_results(results: list[CheckResult], *, show_all: bool = False) -> str:
@@ -1197,7 +1264,13 @@ def format_results(results: list[CheckResult], *, show_all: bool = False) -> str
1197
1264
  lines.append(f"All {len(results)} checks OK.")
1198
1265
  has_issues = any(r.status in (Status.WARN, Status.FAIL) for r in results)
1199
1266
  if has_issues:
1200
- lines.append("\nTip: run `ccst install-everything --apply` to sync skills, hooks, shell, and CLAUDE.md")
1267
+ lines.append("")
1268
+ lines.append(
1269
+ wrap_block(
1270
+ "Tip: run `ccst install-everything --apply` to sync skills, hooks, shell, and CLAUDE.md",
1271
+ indent="",
1272
+ )
1273
+ )
1201
1274
  if not show_all:
1202
1275
  lines.append("\nTo see full doctor output use --all argument")
1203
1276
  return "\n".join(lines)
@@ -0,0 +1,35 @@
1
+ """Terminal-width word wrapping shared by `ccst doctor` and the hooks install output."""
2
+ from __future__ import annotations
3
+
4
+ import shutil
5
+ import textwrap
6
+
7
+ FALLBACK_WIDTH = 80
8
+ MIN_WIDTH = 40
9
+
10
+
11
+ def effective_width() -> int:
12
+ """Width to wrap to: the detected terminal width, never below MIN_WIDTH.
13
+
14
+ `shutil.get_terminal_size` reads `COLUMNS`, then the real terminal via the process's stdout
15
+ file descriptor (so an in-process `redirect_stdout` capture still sees the real terminal),
16
+ and falls back to FALLBACK_WIDTH when neither is available. It is called through the module
17
+ attribute so tests can patch it.
18
+ """
19
+ return max(shutil.get_terminal_size(fallback=(FALLBACK_WIDTH, 24)).columns, MIN_WIDTH)
20
+
21
+
22
+ def wrap_block(text: str, *, indent: str = " ") -> str:
23
+ """Word-wrap `text` to the effective width, indenting every line.
24
+
25
+ Words are never split: a path, hyphenated name or backticked command that has no whitespace
26
+ stays on one line, and a single token longer than the width overflows rather than breaking.
27
+ """
28
+ return textwrap.fill(
29
+ text,
30
+ width=effective_width(),
31
+ initial_indent=indent,
32
+ subsequent_indent=indent,
33
+ break_long_words=False,
34
+ break_on_hyphens=False,
35
+ )
@@ -206,8 +206,23 @@ claude-code-usage --version # should print the version
206
206
  uv sync --extra dev
207
207
  uv run pytest # all 350+ tests should pass
208
208
 
209
- # 4. Install ccusage globally so reconciliation works
210
- # Install bun first if needed: https://bun.com/docs/installation
209
+ # 4a. Install bun (needed to install ccusage). Same command on macOS and Linux/WSL.
210
+ # Linux/WSL: install `unzip` first (e.g. `sudo apt install unzip`), and install bun
211
+ # inside the Linux environment - a Windows-side bun is not visible to WSL tools.
212
+ # macOS alternative: `brew install oven-sh/bun/bun`.
213
+ curl -fsSL https://bun.sh/install | bash
214
+ bun --version
215
+
216
+ # 4b. Make bun's global bin dir (default ~/.bun/bin) resolvable on PATH, including in
217
+ # non-interactive shells (hooks, scheduled jobs). The installer edits the rc file of the
218
+ # shell you ran it from, which non-interactive shells often skip; also add it to
219
+ # ~/.zshenv (zsh) or ~/.profile (bash):
220
+ # export PATH="$HOME/.bun/bin:$PATH"
221
+ # claude-code-usage also falls back to ~/.bun/bin ($BUN_INSTALL/bin) on its own, so
222
+ # reconcile works without this; the PATH edit is what makes the `ccusage` command itself
223
+ # (e.g. `ccusage blocks`) available.
224
+
225
+ # 4c. Install ccusage globally so reconciliation works
211
226
  bun add -g ccusage
212
227
  ccusage --version
213
228
 
@@ -223,6 +238,11 @@ claude-code-usage warm-cache
223
238
  claude-code-usage reconcile
224
239
  ```
225
240
 
241
+ **Existing machine that already has CCST:** `ccst doctor` reports `deps:bun` and `deps:ccusage`
242
+ (WARN when missing or not on `PATH`, with the exact fix). `ccst install-everything` runs doctor at
243
+ its end, so upgrading surfaces a missing install; CCST never installs bun or ccusage itself. Mute
244
+ with `ccst doctor --mute deps:ccusage` if you deliberately skip it.
245
+
226
246
  The repo can also be sourced from GitHub directly (via the upstream
227
247
  remote) if you don't want to clone it - see the README's "Install"
228
248
  section for that path.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-session-tools
3
- Version: 3.6.1
3
+ Version: 3.7.1
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
@@ -105,7 +105,7 @@ bundled scheduled jobs:
105
105
  | **`session-tag`** (SessionStart) | Writes a `<uuid>.tag` file so `claude-code-usage` can map session UUIDs to human-readable names. |
106
106
  | **`last-screenshot`** (UserPromptSubmit) | Resolves your newest screenshot for the `>lss` token and injects its path. Requires `CCST_SCREENSHOT_DIR`. |
107
107
  | **`bash-hard-deny`** (PreToolUse) | Categorical hard-deny gate for Bash: destructive file ops (`rm`/`rmdir`/`unlink`/`shred`, incl. inline python/node and script-file/heredoc forms), delete-by-move to a tmp-like location, `git branch`/`git push` branch deletion, `gh api`/`gh release` delete, destructive curl/wget methods, `sudo`, and direct telemetry-log reads. Runs before every other Bash hook; a match here can never be overridden in-session (the message tells you to run the command yourself in a separate terminal). |
108
- | **`bash-security-review`** (PreToolUse) | Tiered Bash command security review with an allowlist cache and LLM fallback. |
108
+ | **`bash-security-review`** (PreToolUse) | Tiered Bash command security review: trivial/read-only pre-filters, heuristic escalation, a reviewed-script allowlist (script path + content hash, managed with `ccst hooks allowlist list\|add\|remove\|verify`), a command cache, and a slimmed `claude -p` LLM fallback (~770 tokens per review instead of ~50k+). |
109
109
  | **`marker-allow`** (PreToolUse) | Auto-approves a bare `touch` of a skill marker under `~/.cache/claude/markers/` (and nothing else), so marker-gated skills can refresh their TTL marker without a permission prompt. |
110
110
  | **`confirm-8digit`** (PreToolUse) | Blocks a configurable set of high-stakes tool calls unless the user repeats back an 8-digit confirmation code. |
111
111
  | **`after-response`** (Stop) | Touches a `.last-active` sentinel so `ccs --order-by active` can sort sessions by recency of Claude activity. |
@@ -151,7 +151,7 @@ If you've ever tried to remember which `1f4a8b3c-...` UUID is the session where
151
151
 
152
152
  - **Python 3.11+** (3.12+ recommended)
153
153
  - **The `claude` CLI on your `$PATH`.** Install it first via [the official Claude Code instructions](https://docs.anthropic.com/en/docs/claude-code/setup) and verify with `claude --version`.
154
- - **`ccusage` (optional)** - if on `$PATH`, `claude-code-usage reconcile` cross-checks dollar totals against it. Skipped gracefully if missing.
154
+ - **`ccusage` (optional)** - if on `$PATH` (or in bun's global bin dir, `~/.bun/bin`), `claude-code-usage reconcile` cross-checks dollar totals against it. Skipped gracefully if missing. Needs `bun` to install; `ccst doctor` reports both under `deps:bun` / `deps:ccusage` and prints the install command. See the `analyse-cc-usage` skill's setup section.
155
155
  - **`ripgrep` (optional)** - `ccs --contents` prefers `rg`; falls back to threaded Python `grep` if missing.
156
156
 
157
157
  ### Install and set up
@@ -23,6 +23,7 @@ src/cc_session_tools/cli/migrate_telemetry.py
23
23
  src/cc_session_tools/config/hooks-bundle.json
24
24
  src/cc_session_tools/config/session-tag-hook.json
25
25
  src/cc_session_tools/lib/__init__.py
26
+ src/cc_session_tools/lib/bun_tools.py
26
27
  src/cc_session_tools/lib/claude_flags.py
27
28
  src/cc_session_tools/lib/claude_md_install.py
28
29
  src/cc_session_tools/lib/context_overrides.py
@@ -48,6 +49,7 @@ src/cc_session_tools/lib/sessions_repair.py
48
49
  src/cc_session_tools/lib/shell_install.py
49
50
  src/cc_session_tools/lib/tasklist.py
50
51
  src/cc_session_tools/lib/telemetry_store.py
52
+ src/cc_session_tools/lib/text_wrap.py
51
53
  src/cc_session_tools/lib/messaging/__init__.py
52
54
  src/cc_session_tools/lib/messaging/addressing.py
53
55
  src/cc_session_tools/lib/messaging/cursor.py
@@ -176,6 +178,7 @@ src/hooks/normalise.py
176
178
  src/hooks/pdata_sync.py
177
179
  src/hooks/pending_migration.py
178
180
  src/hooks/pending_rename.py
181
+ src/hooks/script_allowlist.py
179
182
  src/hooks/session_tag.py
180
183
  src/hooks/stats.py
181
184
  src/hooks/telemetry.py
@@ -187,6 +190,8 @@ tests/test_after_response.py
187
190
  tests/test_attribution.py
188
191
  tests/test_bash_hard_deny.py
189
192
  tests/test_bash_security_review.py
193
+ tests/test_bash_security_review_allowlist.py
194
+ tests/test_bun_tools.py
190
195
  tests/test_cache.py
191
196
  tests/test_cache_sqlite.py
192
197
  tests/test_ccr_orphans.py
@@ -204,6 +209,7 @@ tests/test_ccst_doctor.py
204
209
  tests/test_ccst_gc_prune.py
205
210
  tests/test_ccst_gc_report.py
206
211
  tests/test_ccst_hook_dispatcher.py
212
+ tests/test_ccst_hooks_allowlist.py
207
213
  tests/test_ccst_hooks_install_prune.py
208
214
  tests/test_ccst_hooks_install_zero_arg.py
209
215
  tests/test_ccst_hooks_uninstall.py
@@ -242,6 +248,7 @@ tests/test_doctor_mutes.py
242
248
  tests/test_empty_session.py
243
249
  tests/test_hook_cache.py
244
250
  tests/test_hooks_install.py
251
+ tests/test_hooks_install_records.py
245
252
  tests/test_install_sync.py
246
253
  tests/test_last_screenshot.py
247
254
  tests/test_levenshtein.py
@@ -268,6 +275,7 @@ tests/test_roots.py
268
275
  tests/test_rules.py
269
276
  tests/test_scheduler_bundled_jobs.py
270
277
  tests/test_schema.py
278
+ tests/test_script_allowlist.py
271
279
  tests/test_session_names.py
272
280
  tests/test_session_tag.py
273
281
  tests/test_sessions.py
@@ -276,5 +284,6 @@ tests/test_sessions_repair.py
276
284
  tests/test_tasklist.py
277
285
  tests/test_telemetry.py
278
286
  tests/test_telemetry_store.py
287
+ tests/test_text_wrap.py
279
288
  tests/test_transcript.py
280
289
  tests/test_worklog_guard.py
@@ -10,11 +10,12 @@ that's our domain.
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
13
- import shutil
14
13
  import subprocess
15
14
  from dataclasses import dataclass, field
16
15
  from typing import Any
17
16
 
17
+ from cc_session_tools.lib import bun_tools
18
+
18
19
 
19
20
  @dataclass
20
21
  class DailyEntry:
@@ -52,11 +53,11 @@ class ReconcileDiff:
52
53
 
53
54
 
54
55
  class CcusageNotInstalled(RuntimeError):
55
- """Raised when ccusage is not on PATH."""
56
+ """Raised when ccusage is neither on PATH nor in bun's global bin directory."""
56
57
 
57
58
 
58
59
  def is_available() -> bool:
59
- return shutil.which("ccusage") is not None
60
+ return bun_tools.resolve("ccusage") is not None
60
61
 
61
62
 
62
63
  def run_daily(since: str | None = None, until: str | None = None,
@@ -65,9 +66,14 @@ def run_daily(since: str | None = None, until: str | None = None,
65
66
 
66
67
  Dates are formatted as YYYYMMDD per ccusage's CLI.
67
68
  """
68
- if not is_available():
69
- raise CcusageNotInstalled("ccusage CLI is not on PATH; bun add -g ccusage")
70
- cmd = ["ccusage", "daily", "--json"]
69
+ resolved = bun_tools.resolve("ccusage")
70
+ if resolved is None:
71
+ raise CcusageNotInstalled(
72
+ "ccusage not found on PATH or in bun's global bin directory. Follow the "
73
+ "analyse-cc-usage skill's setup step 4 (install bun, then "
74
+ f"`{bun_tools.CCUSAGE_INSTALL_COMMAND}`); `ccst doctor` shows the current state."
75
+ )
76
+ cmd = [resolved.path, "daily", "--json"]
71
77
  if offline:
72
78
  cmd.append("--offline")
73
79
  if since: