cc-session-tools 3.3.2__tar.gz → 3.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cc_session_tools-3.3.2/src/cc_session_tools.egg-info → cc_session_tools-3.5.2}/PKG-INFO +4 -4
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/README.md +3 -3
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/pyproject.toml +11 -2
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/ccst.py +51 -3
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/doctor.py +32 -4
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/resolve.py +1 -1
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/ledger.py +1 -1
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/telemetry_store.py +4 -4
- cc_session_tools-3.5.2/src/cc_session_tools/skills/extract-pptx/SKILL.md +111 -0
- cc_session_tools-3.5.2/src/cc_session_tools/skills/extract-pptx/scripts/extract_pptx.py +219 -0
- cc_session_tools-3.5.2/src/cc_session_tools/skills/extract-to-text/SKILL.md +112 -0
- cc_session_tools-3.5.2/src/cc_session_tools/skills/extract-to-text/scripts/extract_to_text.py +367 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/pm-pdata-do-init/SKILL.md +11 -11
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/pm-pdata-resolve-conflicts/SKILL.md +13 -13
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/update-command-cache/SKILL.md +4 -4
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/update-command-cache/scripts/update_command_cache.py +5 -5
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2/src/cc_session_tools.egg-info}/PKG-INFO +4 -4
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools.egg-info/SOURCES.txt +4 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/bash_hard_deny.py +4 -4
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/bash_security_review.py +7 -7
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/cache.py +2 -2
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/catchup.py +3 -3
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/confirm_8digit.py +23 -14
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/markers.py +3 -3
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/pdata_sync.py +3 -3
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/telemetry.py +1 -1
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/telemetry_query.py +1 -1
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/telemetry_trim.py +1 -1
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/worklog_guard.py +3 -3
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_bash_hard_deny.py +12 -12
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_bash_security_review.py +29 -29
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_cache_sqlite.py +4 -4
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_doctor.py +95 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_install_everything.py +19 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_pdata_sync_cli.py +1 -1
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_skills_install.py +68 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_confirm_8digit.py +130 -64
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_marker_allow.py +1 -1
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_migrate_telemetry.py +2 -2
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_telemetry.py +1 -1
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_telemetry_store.py +3 -3
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_worklog_guard.py +5 -5
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/LICENSE +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/setup.cfg +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/__init__.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/__init__.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/ccd.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/ccmsg.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/ccr.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/ccs.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/ccsched.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/migrate_ccmsg.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/migrate_ccsched.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/migrate_sessions_db.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/cli/migrate_telemetry.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/config/hooks-bundle.json +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/config/session-tag-hook.json +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/hooks_install.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/__init__.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/claude_flags.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/claude_md_install.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/context_overrides.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/db.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/debug.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/doctor_mutes.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/hook_registry.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/install_sync.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/levenshtein.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/machine_identity.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/__init__.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/addressing.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/cursor.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/lock.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/message.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/move_safety.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/repository.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/retention.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/service.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/messaging/store.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/occupancy.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/paths.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/__init__.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/backup.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/classify.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/cutover.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/dump.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/formatting.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/importers.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/init_paths.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/init_service.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/manifest.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/naming.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/rehydrate.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/rename_group.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/reorganize.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/repository.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/service.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/session_output.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/store.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/sync_check.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/sync_lock.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/sync_notify.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/vector_clock.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/vector_clock_store.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/verify.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/pdata/write_log.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/picker.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/proc_lock.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/prompts.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/roots.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/rules.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/__init__.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/bundled_jobs.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/cadence.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/cursor.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/digest.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/due.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/duration.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/jobspec.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/lock.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/notify.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/reconcile.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/registry.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/runner.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/state.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/store.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/surface.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/throttle.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/worker.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/session_gc.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/sessions.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/sessions_db.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/sessions_repair.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/shell_install.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/tasklist.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/prompts/pdata-migration-claude-md-update.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/prompts/pdata-migration-skills-update.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/analyse-cc-usage/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/clean-hook-sessions/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/clean-hook-sessions/scripts/clean-hook-sessions.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/compact-smart/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/context-override/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/delete-sessions/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/delete-sessions/scripts/delete_sessions.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/do-executor-critic-assessor-loop/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/find-claude-code-session/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/generate-8digit-code/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/generate-8digit-code/scripts/generate_8digit_code.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/list-empty-sessions/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/list-empty-sessions/scripts/list_empty_sessions.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/manage-recurring-cc-jobs-using-ccsched/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/move-session/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/move-session/hooks/sessionstart-pending-rename.sh +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/move-session/scripts/cc_session_rules.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/move-session/scripts/move_session.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/pm-pdata-design-schema/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/pm-pdata-do-audit-and-prepare-to-migrate/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/pm-pdata-do-manual-migration/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/pm-project-layout-reference/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/pm-update-central-files/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/reduce-persistent-context/CALIBRATION.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/reduce-persistent-context/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/analyze_context.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/duplicates.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/measure.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/report.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/tokens.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/reduce-persistent-context/scripts/usage.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/select-agent-model/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/skills/send-session-message/SKILL.md +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools.egg-info/dependency_links.txt +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools.egg-info/entry_points.txt +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools.egg-info/requires.txt +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools.egg-info/top_level.txt +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/__init__.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/attribution.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/cache.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/ccusage_wrapper.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/data/pricing.json +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/parent_inference.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/parser.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/pricing.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/query.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/report.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/schema.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/claude_code_usage/session_names.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/__init__.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/after_response.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/context_window_warning.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/last_screenshot.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/marker_allow.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/messaging_deliver.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/model_info.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/normalise.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/pending_migration.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/pending_rename.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/session_tag.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/stats.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/hooks/transcript.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_after_response.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_attribution.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_cache.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccr_orphans.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccs_emptiness.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccs_eta.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccs_list_mode.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccs_scope_flags.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccs_sentinel_sort.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccs_session_counts.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_bundle_discovery.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_ccsched_jobs_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_context_override_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_gc_prune.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_gc_report.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_hook_dispatcher.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_hooks_install_prune.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_hooks_install_zero_arg.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_hooks_uninstall.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_machine_identity_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_main_gate.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_pdata_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_pdata_init_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_pdata_reconcile_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_pdata_rename_group_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_pdata_reorganize_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_pdata_verify_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_repair_sessions_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_sessions_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_shell_install.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_skills_uninstall.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_telemetry_query.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccst_telemetry_trim.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_ccusage_wrapper.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_claude_flags.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_claude_md_install.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_cli.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_cli_ccd.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_cli_ccd_dry_run.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_cli_ccr.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_cli_ccs.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_cli_version.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_context_overrides.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_context_window_warning.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_db.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_debug.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_doctor_mutes.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_empty_session.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_hook_cache.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_hooks_install.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_install_sync.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_last_screenshot.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_levenshtein.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_machine_identity.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_migrate_ccmsg.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_migrate_ccsched.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_migrate_sessions_db.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_model_info.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_normalise.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_occupancy.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_parent_inference.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_parser.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_paths.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_pending_migration_hook.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_picker.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_pricing.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_proc_lock.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_prompts.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_query.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_report.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_roots.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_rules.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_scheduler_bundled_jobs.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_schema.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_session_names.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_session_tag.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_sessions.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_sessions_db.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_sessions_repair.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_tasklist.py +0 -0
- {cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/tests/test_transcript.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cc-session-tools
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.2
|
|
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
|
|
@@ -569,7 +569,7 @@ Translates a natural-language cadence request ("run my Tesco shop every other Su
|
|
|
569
569
|
|
|
570
570
|
### `update-command-cache`
|
|
571
571
|
|
|
572
|
-
Curates the SHA-256 command cache the `bash-security-review` hook reads (`
|
|
572
|
+
Curates the SHA-256 command cache the `bash-security-review` hook reads (`CCST_USE_COMMAND_CACHE=1`). Triggers on "update the command cache", "sweep the fires log for cacheable commands", "promote my recent claude-CLI safe fires". Reads recent `safe`-verdict fires from `telemetry.db`, identifies commands not yet cached, presents them for approval, and records approved ones. Also supports manual `--remove`/`--flip` on existing entries.
|
|
573
573
|
|
|
574
574
|
### `clean-hook-sessions`
|
|
575
575
|
|
|
@@ -679,7 +679,7 @@ Both hooks are included in the standard bundle and installed by
|
|
|
679
679
|
`ccst hooks install --apply`. Surfacing is per-session (per-session cursor), so
|
|
680
680
|
each session sees each completed run exactly once. Failures never block the
|
|
681
681
|
session; every action is recorded to the shared `telemetry.db` (SQLite, under
|
|
682
|
-
`~/.local/share/claude/`, overridable via `
|
|
682
|
+
`~/.local/share/claude/`, overridable via `CCST_HOOKS_DIR`) telemetry ledger —
|
|
683
683
|
query it with `ccst telemetry query`.
|
|
684
684
|
|
|
685
685
|
### `manage-recurring-cc-jobs-using-ccsched` skill
|
|
@@ -840,7 +840,7 @@ real risk, which is why this hook blocks rather than just warning like
|
|
|
840
840
|
- **Only fires if a WORKLOG.md already exists** - it never forces you to
|
|
841
841
|
create one, and never blocks automatic (non-`/compact`) compaction, which
|
|
842
842
|
can happen mid-task in an unattended session with nobody present to react.
|
|
843
|
-
- **Escape hatch:** set `
|
|
843
|
+
- **Escape hatch:** set `CCST_ALLOW_STALE_WORKLOG=1` to bypass the block for
|
|
844
844
|
one `/compact`.
|
|
845
845
|
|
|
846
846
|
### Running modules directly (debugging only)
|
|
@@ -512,7 +512,7 @@ Translates a natural-language cadence request ("run my Tesco shop every other Su
|
|
|
512
512
|
|
|
513
513
|
### `update-command-cache`
|
|
514
514
|
|
|
515
|
-
Curates the SHA-256 command cache the `bash-security-review` hook reads (`
|
|
515
|
+
Curates the SHA-256 command cache the `bash-security-review` hook reads (`CCST_USE_COMMAND_CACHE=1`). Triggers on "update the command cache", "sweep the fires log for cacheable commands", "promote my recent claude-CLI safe fires". Reads recent `safe`-verdict fires from `telemetry.db`, identifies commands not yet cached, presents them for approval, and records approved ones. Also supports manual `--remove`/`--flip` on existing entries.
|
|
516
516
|
|
|
517
517
|
### `clean-hook-sessions`
|
|
518
518
|
|
|
@@ -622,7 +622,7 @@ Both hooks are included in the standard bundle and installed by
|
|
|
622
622
|
`ccst hooks install --apply`. Surfacing is per-session (per-session cursor), so
|
|
623
623
|
each session sees each completed run exactly once. Failures never block the
|
|
624
624
|
session; every action is recorded to the shared `telemetry.db` (SQLite, under
|
|
625
|
-
`~/.local/share/claude/`, overridable via `
|
|
625
|
+
`~/.local/share/claude/`, overridable via `CCST_HOOKS_DIR`) telemetry ledger —
|
|
626
626
|
query it with `ccst telemetry query`.
|
|
627
627
|
|
|
628
628
|
### `manage-recurring-cc-jobs-using-ccsched` skill
|
|
@@ -783,7 +783,7 @@ real risk, which is why this hook blocks rather than just warning like
|
|
|
783
783
|
- **Only fires if a WORKLOG.md already exists** - it never forces you to
|
|
784
784
|
create one, and never blocks automatic (non-`/compact`) compaction, which
|
|
785
785
|
can happen mid-task in an unattended session with nobody present to react.
|
|
786
|
-
- **Escape hatch:** set `
|
|
786
|
+
- **Escape hatch:** set `CCST_ALLOW_STALE_WORKLOG=1` to bypass the block for
|
|
787
787
|
one `/compact`.
|
|
788
788
|
|
|
789
789
|
### Running modules directly (debugging only)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cc-session-tools"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.5.2"
|
|
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" }
|
|
@@ -72,7 +72,7 @@ cc_session_tools = ["skills/**/*", "config/*", "prompts/**/*"]
|
|
|
72
72
|
cc_session_tools = ["**/tests/*", "**/__pycache__/*"]
|
|
73
73
|
|
|
74
74
|
[tool.pytest.ini_options]
|
|
75
|
-
testpaths = ["tests", "tests/scheduler", "src/cc_session_tools/skills/move-session/tests", "src/cc_session_tools/skills/list-empty-sessions/tests", "src/cc_session_tools/skills/delete-sessions/tests", "src/cc_session_tools/skills/generate-8digit-code/tests", "src/cc_session_tools/skills/reduce-persistent-context/tests", "src/cc_session_tools/skills/update-command-cache/tests"]
|
|
75
|
+
testpaths = ["tests", "tests/scheduler", "src/cc_session_tools/skills/move-session/tests", "src/cc_session_tools/skills/list-empty-sessions/tests", "src/cc_session_tools/skills/delete-sessions/tests", "src/cc_session_tools/skills/generate-8digit-code/tests", "src/cc_session_tools/skills/reduce-persistent-context/tests", "src/cc_session_tools/skills/update-command-cache/tests", "src/cc_session_tools/skills/extract-pptx/tests", "src/cc_session_tools/skills/extract-to-text/tests"]
|
|
76
76
|
addopts = "-q"
|
|
77
77
|
pythonpath = ["src"]
|
|
78
78
|
|
|
@@ -82,8 +82,17 @@ dev = [
|
|
|
82
82
|
"pytest>=7",
|
|
83
83
|
"pytest-mock>=3.12",
|
|
84
84
|
"types-PyYAML>=6.0",
|
|
85
|
+
"python-pptx>=1.0",
|
|
86
|
+
"extract-msg>=0.45",
|
|
87
|
+
"pillow>=10.0",
|
|
88
|
+
"pytesseract>=0.3",
|
|
89
|
+
"types-openpyxl>=3.1",
|
|
85
90
|
]
|
|
86
91
|
|
|
87
92
|
[[tool.mypy.overrides]]
|
|
88
93
|
module = "tiktoken_tools.*"
|
|
89
94
|
ignore_missing_imports = true
|
|
95
|
+
|
|
96
|
+
[[tool.mypy.overrides]]
|
|
97
|
+
module = "pytesseract.*"
|
|
98
|
+
ignore_missing_imports = true
|
|
@@ -96,6 +96,7 @@ import importlib
|
|
|
96
96
|
import json
|
|
97
97
|
import os
|
|
98
98
|
import sys
|
|
99
|
+
from dataclasses import dataclass
|
|
99
100
|
from enum import Enum
|
|
100
101
|
from pathlib import Path
|
|
101
102
|
from typing import TYPE_CHECKING, Any, TextIO, cast
|
|
@@ -239,6 +240,37 @@ def _decide_action(skill_src: Path, target_dir: Path) -> tuple[SkillAction, Path
|
|
|
239
240
|
return SkillAction.NON_SYMLINK_EXISTS, dest
|
|
240
241
|
|
|
241
242
|
|
|
243
|
+
@dataclass(frozen=True)
|
|
244
|
+
class StaleSkillSymlink:
|
|
245
|
+
dest: Path
|
|
246
|
+
target: Path
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _find_stale_skill_symlinks(source_dir: Path, target_dir: Path) -> list[StaleSkillSymlink]:
|
|
250
|
+
"""Return every symlink under target_dir left behind by a rename or removal from
|
|
251
|
+
source_dir: dangling (its target no longer exists) AND created by a previous install from
|
|
252
|
+
source_dir (its stored target's parent resolves to source_dir). `ccst skills install` only
|
|
253
|
+
ever creates symlinks for skills currently in source_dir, so a name it no longer bundles is
|
|
254
|
+
never revisited by the create/repoint loop above — this is the matching subtractive pass.
|
|
255
|
+
|
|
256
|
+
A dangling symlink pointing somewhere other than source_dir is not ours to remove (e.g. a
|
|
257
|
+
user's own broken symlink), and neither is a non-symlink entry — both are left untouched.
|
|
258
|
+
"""
|
|
259
|
+
if not target_dir.is_dir():
|
|
260
|
+
return []
|
|
261
|
+
resolved_source = source_dir.resolve()
|
|
262
|
+
stale: list[StaleSkillSymlink] = []
|
|
263
|
+
for entry in sorted(target_dir.iterdir()):
|
|
264
|
+
if not entry.is_symlink() or entry.exists():
|
|
265
|
+
continue
|
|
266
|
+
raw_target = Path(os.readlink(entry))
|
|
267
|
+
target = raw_target if raw_target.is_absolute() else (entry.parent / raw_target)
|
|
268
|
+
if target.parent.resolve() != resolved_source:
|
|
269
|
+
continue
|
|
270
|
+
stale.append(StaleSkillSymlink(dest=entry, target=target))
|
|
271
|
+
return stale
|
|
272
|
+
|
|
273
|
+
|
|
242
274
|
def _cmd_skills_install(args: argparse.Namespace) -> int:
|
|
243
275
|
# Resolve source
|
|
244
276
|
if args.source:
|
|
@@ -273,8 +305,16 @@ def _cmd_skills_install(args: argparse.Namespace) -> int:
|
|
|
273
305
|
for action, skill_src, dest in decisions:
|
|
274
306
|
print(f"{skill_src.name:<{col_w}} {action.value}")
|
|
275
307
|
|
|
308
|
+
stale = _find_stale_skill_symlinks(source_dir, target_dir)
|
|
309
|
+
if stale:
|
|
310
|
+
print("\nStale symlink(s) (skill no longer bundled):")
|
|
311
|
+
for entry in stale:
|
|
312
|
+
print(f" {entry.dest.name} -> {entry.target}")
|
|
313
|
+
|
|
276
314
|
if not args.apply:
|
|
277
315
|
print(f"\nDry run — re-run with --apply to create symlinks in {target_dir}")
|
|
316
|
+
if stale:
|
|
317
|
+
print(f"({len(stale)} stale symlink(s) above would also be removed)")
|
|
278
318
|
return 0
|
|
279
319
|
|
|
280
320
|
# Perform writes
|
|
@@ -315,12 +355,20 @@ def _cmd_skills_install(args: argparse.Namespace) -> int:
|
|
|
315
355
|
linked.append(dest)
|
|
316
356
|
print(f" linked: {dest} -> {skill_src}")
|
|
317
357
|
|
|
358
|
+
pruned: list[Path] = []
|
|
359
|
+
for entry in stale:
|
|
360
|
+
entry.dest.unlink()
|
|
361
|
+
pruned.append(entry.dest)
|
|
362
|
+
print(f" removed stale: {entry.dest}")
|
|
363
|
+
|
|
318
364
|
print()
|
|
319
365
|
if linked:
|
|
320
366
|
print(f"Linked {len(linked)} skill(s) in {target_dir}")
|
|
321
367
|
if skipped:
|
|
322
368
|
print(f"Skipped {len(skipped)} (already correct)")
|
|
323
|
-
if
|
|
369
|
+
if pruned:
|
|
370
|
+
print(f"Removed {len(pruned)} stale symlink(s) no longer bundled")
|
|
371
|
+
if not linked and not skipped and not pruned and not failed:
|
|
324
372
|
print(f"Nothing to do in {target_dir}")
|
|
325
373
|
|
|
326
374
|
if failed:
|
|
@@ -2757,7 +2805,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
2757
2805
|
"--hooks-dir",
|
|
2758
2806
|
default=None,
|
|
2759
2807
|
metavar="DIR",
|
|
2760
|
-
help="telemetry.db directory (default:
|
|
2808
|
+
help="telemetry.db directory (default: CCST_HOOKS_DIR or ~/.local/share/claude/)",
|
|
2761
2809
|
)
|
|
2762
2810
|
|
|
2763
2811
|
telemetry_query_parser = telemetry_sub.add_parser(
|
|
@@ -2784,7 +2832,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
2784
2832
|
)
|
|
2785
2833
|
telemetry_query_parser.add_argument(
|
|
2786
2834
|
"--hooks-dir", default=None, metavar="DIR",
|
|
2787
|
-
help="telemetry.db directory (default:
|
|
2835
|
+
help="telemetry.db directory (default: CCST_HOOKS_DIR or ~/.local/share/claude/)",
|
|
2788
2836
|
)
|
|
2789
2837
|
|
|
2790
2838
|
# ---- gc ----
|
|
@@ -14,6 +14,7 @@ import os
|
|
|
14
14
|
import shutil
|
|
15
15
|
import sqlite3
|
|
16
16
|
import subprocess
|
|
17
|
+
import textwrap
|
|
17
18
|
import time
|
|
18
19
|
from dataclasses import dataclass
|
|
19
20
|
from enum import Enum
|
|
@@ -1148,6 +1149,21 @@ def _extract_bundle_hook_names(bundle_path: Path) -> list[str]:
|
|
|
1148
1149
|
return names
|
|
1149
1150
|
|
|
1150
1151
|
|
|
1152
|
+
def _wrap_reason(reason: str, *, indent: str = " ") -> str:
|
|
1153
|
+
"""Word-wrap a check's reason to the detected terminal width.
|
|
1154
|
+
|
|
1155
|
+
Reasons are long free-text sentences (often 100-250+ characters, with
|
|
1156
|
+
embedded paths and remediation commands); printed on a single line they
|
|
1157
|
+
hard-wrap at the terminal edge with no continuation indent, making a
|
|
1158
|
+
wrapped reason indistinguishable from a new, unrelated result. Wrapping
|
|
1159
|
+
to the real terminal width — falling back to 80 columns for non-tty
|
|
1160
|
+
output (piped, redirected, or the scheduled drift-monitor job whose
|
|
1161
|
+
stdout is forwarded to Telegram) — and indenting every line fixes that.
|
|
1162
|
+
"""
|
|
1163
|
+
width = shutil.get_terminal_size(fallback=(80, 24)).columns
|
|
1164
|
+
return textwrap.fill(reason, width=width, initial_indent=indent, subsequent_indent=indent)
|
|
1165
|
+
|
|
1166
|
+
|
|
1151
1167
|
def format_results(results: list[CheckResult], *, show_all: bool = False) -> str:
|
|
1152
1168
|
"""Return a human-readable table of check results.
|
|
1153
1169
|
|
|
@@ -1156,15 +1172,27 @@ def format_results(results: list[CheckResult], *, show_all: bool = False) -> str
|
|
|
1156
1172
|
otherwise dozens of [OK] lines a user has to scroll past to find the one
|
|
1157
1173
|
thing that needs attention. show_all=True reproduces the full table this
|
|
1158
1174
|
function always printed before this parameter existed.
|
|
1175
|
+
|
|
1176
|
+
A non-OK result renders as a two-line record — `[STATUS] name` on its own
|
|
1177
|
+
line, then the reason wrapped and indented below it — since its reason is
|
|
1178
|
+
the long free-text kind `_wrap_reason` exists for. OK rows (only ever
|
|
1179
|
+
shown via show_all) stay a single fixed-width `[OK] name reason` line;
|
|
1180
|
+
their reasons are short and there's nothing to disambiguate from a
|
|
1181
|
+
neighbouring result.
|
|
1159
1182
|
"""
|
|
1160
1183
|
if not results:
|
|
1161
1184
|
return "(no checks ran)"
|
|
1162
1185
|
shown = results if show_all else [r for r in results if r.status is not Status.OK]
|
|
1163
1186
|
lines = []
|
|
1164
1187
|
if shown:
|
|
1165
|
-
|
|
1188
|
+
ok_shown = [r for r in shown if r.status is Status.OK]
|
|
1189
|
+
name_w = max((len(r.name) for r in ok_shown), default=0)
|
|
1166
1190
|
for r in shown:
|
|
1167
|
-
|
|
1191
|
+
if r.status is Status.OK:
|
|
1192
|
+
lines.append(f"[{r.status.value:<4}] {r.name:<{name_w}} {r.reason}")
|
|
1193
|
+
else:
|
|
1194
|
+
lines.append(f"[{r.status.value:<4}] {r.name}")
|
|
1195
|
+
lines.append(_wrap_reason(r.reason))
|
|
1168
1196
|
elif not show_all:
|
|
1169
1197
|
lines.append(f"All {len(results)} checks OK.")
|
|
1170
1198
|
has_issues = any(r.status in (Status.WARN, Status.FAIL) for r in results)
|
|
@@ -1201,10 +1229,10 @@ def format_drift_report(unmuted: list[CheckResult], *, muted_count: int) -> str:
|
|
|
1201
1229
|
"""
|
|
1202
1230
|
if not unmuted:
|
|
1203
1231
|
return ""
|
|
1204
|
-
name_w = max(len(r.name) for r in unmuted)
|
|
1205
1232
|
lines = ["ccst doctor: un-muted drift detected —"]
|
|
1206
1233
|
for r in unmuted:
|
|
1207
|
-
lines.append(f" [{r.status.value:<4}] {r.name
|
|
1234
|
+
lines.append(f" [{r.status.value:<4}] {r.name}")
|
|
1235
|
+
lines.append(_wrap_reason(r.reason))
|
|
1208
1236
|
lines.append("")
|
|
1209
1237
|
lines.append(
|
|
1210
1238
|
"Acknowledge an item with: ccst doctor --mute <name>"
|
|
@@ -7,7 +7,7 @@ overwrite (that is rehydrate.py's job for the clean fast-forward case; this modu
|
|
|
7
7
|
`ccst pdata resolve` reaches for once vector_clock.compare() has already reported a genuine FORK).
|
|
8
8
|
|
|
9
9
|
Never auto-merges, never silently keeps one side and discards the other — every record surfaced
|
|
10
|
-
here needs an explicit choice from the caller (ultimately
|
|
10
|
+
here needs an explicit choice from the caller (ultimately the user, via the CLI/skill), matching the
|
|
11
11
|
existing `pm-pdata-resolve-conflicts` skill's single-file-conflict framing exactly. Every
|
|
12
12
|
record: `apply_resolution` is all-or-nothing over the whole diff, for the reasons in its own
|
|
13
13
|
docstring.
|
{cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/scheduler/ledger.py
RENAMED
|
@@ -50,7 +50,7 @@ class LedgerEntry:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def _hooks_dir() -> Path:
|
|
53
|
-
"""The telemetry.db directory: the
|
|
53
|
+
"""The telemetry.db directory: the CCST_HOOKS_DIR override when set,
|
|
54
54
|
else telemetry_store's default. Kept as a thin wrapper so catchup.py's
|
|
55
55
|
existing ``ledger._hooks_dir()`` call keeps working unchanged."""
|
|
56
56
|
return telemetry_store.hooks_dir()
|
{cc_session_tools-3.3.2 → cc_session_tools-3.5.2}/src/cc_session_tools/lib/telemetry_store.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Shared schema,
|
|
1
|
+
"""Shared schema, CCST_HOOKS_DIR resolution, and connection helper for
|
|
2
2
|
telemetry.db. Single source of truth so hooks.telemetry (writer),
|
|
3
3
|
hooks.telemetry_trim, hooks.telemetry_query, and
|
|
4
4
|
lib.scheduler.ledger (catch-up reader/writer) can never point at different
|
|
@@ -35,7 +35,7 @@ from pathlib import Path
|
|
|
35
35
|
|
|
36
36
|
from cc_session_tools.lib import db, paths
|
|
37
37
|
|
|
38
|
-
HOOKS_DIR_ENV = "
|
|
38
|
+
HOOKS_DIR_ENV = "CCST_HOOKS_DIR"
|
|
39
39
|
DB_FILENAME = "telemetry.db"
|
|
40
40
|
|
|
41
41
|
# Marker name recorded in the migrations table by cli.migrate_telemetry once
|
|
@@ -80,14 +80,14 @@ CREATE INDEX IF NOT EXISTS idx_catchup_events_job_id ON catchup_events(job_id);
|
|
|
80
80
|
|
|
81
81
|
# Computed once at import time; tests override it with
|
|
82
82
|
# monkeypatch.setattr(telemetry_store, "_DEFAULT_HOOKS_DIR", tmp_path) to
|
|
83
|
-
# exercise the "
|
|
83
|
+
# exercise the "CCST_HOOKS_DIR unset" production-default path, matching the
|
|
84
84
|
# existing repo-wide convention for a module-level default.
|
|
85
85
|
_DEFAULT_HOOKS_DIR = paths.data_home()
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def hooks_dir(explicit: Path | None = None) -> Path:
|
|
89
89
|
"""Resolve the telemetry.db directory: explicit override, else
|
|
90
|
-
|
|
90
|
+
CCST_HOOKS_DIR, else the module default."""
|
|
91
91
|
if explicit is not None:
|
|
92
92
|
return explicit
|
|
93
93
|
raw = os.environ.get(HOOKS_DIR_ENV)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: extract-pptx
|
|
3
|
+
description: Use when text needs to be extracted from a .pptx PowerPoint file - slide text, tables, speaker notes, text baked into images (OCR), or embedded file objects (embedded workbooks/documents). Also use when a .pptx needs a sibling print-to-PDF. Triggers on "extract this pptx", "read this powerpoint", "pptx won't extract", "pandoc can't read pptx", empty/failed pptx mirrors in a text-extraction pipeline, or any pptx-to-markdown/pptx-to-pdf task. Do NOT use pandoc for pptx input - pandoc has a markdown writer but no pptx reader.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extract PPTX
|
|
7
|
+
|
|
8
|
+
## Why this exists
|
|
9
|
+
|
|
10
|
+
Pandoc has a markdown *writer* but no `.pptx` *input reader*. Any pipeline that
|
|
11
|
+
routes Office formats through `pandoc -t markdown` (a correct, common pattern
|
|
12
|
+
for preserving Word/RTF tables) silently or loudly fails on every `.pptx` file.
|
|
13
|
+
There is no pandoc flag that fixes this - it needs a different extractor
|
|
14
|
+
entirely. This skill wraps a `python-pptx`-based one. The sibling `extract-to-text`
|
|
15
|
+
skill delegates its own `.pptx`/`.pptm` handling to this script rather than
|
|
16
|
+
re-implementing pptx extraction.
|
|
17
|
+
|
|
18
|
+
## When to use
|
|
19
|
+
|
|
20
|
+
- A `.pptx` file needs its text extracted and pandoc has failed, returned empty
|
|
21
|
+
output, or isn't an option.
|
|
22
|
+
- A text-extraction pipeline's `--check`/audit step shows `.pptx` mirrors as
|
|
23
|
+
empty, missing, or `FAILED`.
|
|
24
|
+
- Text is baked into an image on a slide (a chart export, a scanned page
|
|
25
|
+
pasted as a picture) and needs OCR to be readable.
|
|
26
|
+
- A slide has an embedded object (an embedded Excel sheet, a Word doc) whose
|
|
27
|
+
content needs to be pulled out, not just referenced.
|
|
28
|
+
- A clean, paginated PDF render of a `.pptx` is needed alongside the text
|
|
29
|
+
extraction (evidence packs, archival records).
|
|
30
|
+
|
|
31
|
+
**Do NOT use** `pandoc -t markdown <file>.pptx` - it will fail or silently
|
|
32
|
+
produce nothing useful. Do not hand-roll a second one-off pptx extractor
|
|
33
|
+
either; use or extend this one so behaviour stays consistent across projects.
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
`scripts/extract_pptx.py` opens the file with `python-pptx` and, per slide,
|
|
38
|
+
in order:
|
|
39
|
+
|
|
40
|
+
1. Walks every shape (text frames, tables, and groups recursively) and
|
|
41
|
+
collects text in slide order. Table cells are extracted cell-by-cell, not
|
|
42
|
+
just shape text - this matters the same way it matters for docx tables
|
|
43
|
+
(see the sibling `extract-to-text` skill's docx-table-loss motivation).
|
|
44
|
+
2. If OCR is enabled (default), runs Tesseract over every picture shape's
|
|
45
|
+
image bytes and appends the result labelled as OCR text, distinct from
|
|
46
|
+
slide body text.
|
|
47
|
+
3. Extracts speaker notes if present, labelled distinctly from slide body
|
|
48
|
+
text.
|
|
49
|
+
4. Extracts embedded OLE/package objects (embedded workbooks, documents)
|
|
50
|
+
referenced by the slide's relationships, if `--embeds-dir` is given.
|
|
51
|
+
5. Optionally renders a sibling PDF via headless LibreOffice (`soffice`), if
|
|
52
|
+
`--pdf-out` is given.
|
|
53
|
+
|
|
54
|
+
## Procedure
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python3 ~/.claude/skills/extract-pptx/scripts/extract_pptx.py <source.pptx> \
|
|
58
|
+
--out-md <output.md> \
|
|
59
|
+
[--embeds-dir <dir-for-embedded-files>] \
|
|
60
|
+
[--pdf-out <output.pdf>] \
|
|
61
|
+
[--no-ocr]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `--out-md` is required; the other three are opt-in.
|
|
65
|
+
- OCR is **on by default** and fails loudly (non-zero exit, clear message) if
|
|
66
|
+
`pytesseract`/Pillow or the `tesseract` binary is missing - it does not
|
|
67
|
+
silently skip OCR and produce an incomplete-looking-complete file. Pass
|
|
68
|
+
`--no-ocr` explicitly if OCR isn't wanted for this run.
|
|
69
|
+
- `--embeds-dir` is only written to if the slide actually has embedded
|
|
70
|
+
objects; it's never created empty.
|
|
71
|
+
- `--pdf-out` requires `soffice` (LibreOffice) on `PATH`. If it's missing the
|
|
72
|
+
script exits non-zero with a clear message rather than skipping the PDF
|
|
73
|
+
silently - report that to the user rather than treating the run as
|
|
74
|
+
complete.
|
|
75
|
+
|
|
76
|
+
Verify the output: open `--out-md` and confirm slide count and non-trivial
|
|
77
|
+
content; a `.pptx` with no extractable text on any slide is possible
|
|
78
|
+
(image-only decks) but should be rare - treat an entirely-empty result the
|
|
79
|
+
same way a hand-rolled pipeline treats `empty-output-CHECK-THIS`, not as a
|
|
80
|
+
quiet success.
|
|
81
|
+
|
|
82
|
+
## Refusing fallbacks
|
|
83
|
+
|
|
84
|
+
Do not silently drop OCR, embeds, or the PDF render when a dependency is
|
|
85
|
+
missing and substitute a partial result without saying so. The script is
|
|
86
|
+
built to fail loudly for exactly this reason - surface the failure to the
|
|
87
|
+
user rather than working around it.
|
|
88
|
+
|
|
89
|
+
## Reusing this in a project-local extractor
|
|
90
|
+
|
|
91
|
+
If a project has its own canonical text-extraction script (e.g. one that
|
|
92
|
+
mirrors an archive into markdown files with status metadata), do not import
|
|
93
|
+
this script into it directly across a network/OneDrive boundary - a
|
|
94
|
+
project's canonical script needs to run standalone on every machine it syncs
|
|
95
|
+
to, independent of whether this bundled skill happens to be installed there.
|
|
96
|
+
Port the relevant logic (shape/table/notes walking at minimum) into the
|
|
97
|
+
project's own script instead, matching that script's existing conventions for
|
|
98
|
+
status/output. Treat OCR, embeds, and PDF rendering as separate, explicitly
|
|
99
|
+
opted-into capabilities when porting - they add real dependencies and runtime
|
|
100
|
+
cost that a project's canonical extractor may not want by default.
|
|
101
|
+
|
|
102
|
+
## Troubleshooting
|
|
103
|
+
|
|
104
|
+
| Symptom | Likely cause | Fix |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `pandoc failed` / empty pandoc output on a `.pptx` | pandoc has no pptx reader | Don't use pandoc for pptx; use this skill instead |
|
|
107
|
+
| `ModuleNotFoundError: pytesseract` | OCR deps not installed | `pip install --user pytesseract pillow` (add `--break-system-packages` if the environment is externally-managed) |
|
|
108
|
+
| `tesseract binary is not on PATH` | Tesseract OCR engine not installed | Install `tesseract-ocr` (apt/brew/winget) |
|
|
109
|
+
| `no soffice/libreoffice binary found` | LibreOffice not installed | Install LibreOffice, or run in a container that has it |
|
|
110
|
+
| OCR text is garbled/partial | Low-res or stylised image text | Expected - Tesseract accuracy depends on image quality; note it as approximate in any evidence use |
|
|
111
|
+
| Embeds dir stays empty | Slide genuinely has no embedded OLE/package objects | Not a bug - only shapes with an actual embed relationship produce output |
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Extract a .pptx file's full content: slide text, tables, speaker notes, OCR of
|
|
4
|
+
embedded images, embedded file objects, and (optionally) a sibling print-to-PDF.
|
|
5
|
+
|
|
6
|
+
Used by the `extract-pptx` skill. Exists because pandoc has a markdown *writer*
|
|
7
|
+
but no `.pptx` *reader* - there is no pandoc input format for PowerPoint, so any
|
|
8
|
+
pipeline that routes Office formats through `pandoc -t markdown` (a common,
|
|
9
|
+
otherwise-correct pattern for preserving Word/RTF tables) silently or loudly
|
|
10
|
+
fails on every .pptx file. This script is a standalone python-pptx-based
|
|
11
|
+
replacement for the .pptx case specifically.
|
|
12
|
+
|
|
13
|
+
Dependencies:
|
|
14
|
+
python-pptx pip install python-pptx
|
|
15
|
+
Pillow pip install pillow (only needed for --ocr)
|
|
16
|
+
pytesseract pip install pytesseract (only needed for --ocr)
|
|
17
|
+
tesseract-ocr binary on PATH (only needed for --ocr; apt/brew/winget package)
|
|
18
|
+
soffice (LibreOffice) on PATH (only needed for --pdf-out)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from pptx import Presentation
|
|
31
|
+
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
|
32
|
+
|
|
33
|
+
EMBED_RELTYPE_MARKERS = ("package", "oleObject")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def shape_text(shape) -> list[str]:
|
|
37
|
+
"""Return this shape's own text content as a list of lines. Recurses into
|
|
38
|
+
groups; handles plain text frames and tables. Does not touch images."""
|
|
39
|
+
lines: list[str] = []
|
|
40
|
+
|
|
41
|
+
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
|
|
42
|
+
for sub in shape.shapes:
|
|
43
|
+
lines.extend(shape_text(sub))
|
|
44
|
+
return lines
|
|
45
|
+
|
|
46
|
+
if getattr(shape, "has_table", False) and shape.has_table:
|
|
47
|
+
table = shape.table
|
|
48
|
+
for row in table.rows:
|
|
49
|
+
cells = [cell.text.strip() for cell in row.cells]
|
|
50
|
+
if any(cells):
|
|
51
|
+
lines.append(" | ".join(cells))
|
|
52
|
+
return lines
|
|
53
|
+
|
|
54
|
+
if getattr(shape, "has_text_frame", False) and shape.has_text_frame:
|
|
55
|
+
text = shape.text_frame.text
|
|
56
|
+
if text.strip():
|
|
57
|
+
lines.extend(text.split("\n"))
|
|
58
|
+
return lines
|
|
59
|
+
|
|
60
|
+
return lines
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_slide_images(slide) -> list[tuple[str, bytes, str]]:
|
|
64
|
+
"""Return (shape_name, image_bytes, image_ext) for every picture shape on
|
|
65
|
+
this slide, recursing into groups."""
|
|
66
|
+
found: list[tuple[str, bytes, str]] = []
|
|
67
|
+
|
|
68
|
+
def walk(shapes):
|
|
69
|
+
for shape in shapes:
|
|
70
|
+
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
|
|
71
|
+
walk(shape.shapes)
|
|
72
|
+
elif shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
|
|
73
|
+
image = shape.image
|
|
74
|
+
found.append((shape.name, image.blob, image.ext))
|
|
75
|
+
|
|
76
|
+
walk(slide.shapes)
|
|
77
|
+
return found
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def extract_slide_embeds(slide) -> list[tuple[str, bytes]]:
|
|
81
|
+
"""Return (filename, bytes) for every embedded OLE/package object
|
|
82
|
+
(embedded workbooks, documents, etc.) referenced by this slide."""
|
|
83
|
+
found: list[tuple[str, bytes]] = []
|
|
84
|
+
for rel in slide.part.rels.values():
|
|
85
|
+
if rel.is_external:
|
|
86
|
+
continue
|
|
87
|
+
if not any(marker in rel.reltype for marker in EMBED_RELTYPE_MARKERS):
|
|
88
|
+
continue
|
|
89
|
+
target = rel.target_part
|
|
90
|
+
fname = Path(str(target.partname)).name
|
|
91
|
+
found.append((fname, target.blob))
|
|
92
|
+
return found
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ocr_image(image_bytes: bytes) -> str:
|
|
96
|
+
import io
|
|
97
|
+
|
|
98
|
+
from PIL import Image
|
|
99
|
+
import pytesseract
|
|
100
|
+
|
|
101
|
+
img = Image.open(io.BytesIO(image_bytes))
|
|
102
|
+
return pytesseract.image_to_string(img).strip()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def render_pdf(src: Path, pdf_out: Path, timeout: int) -> None:
|
|
106
|
+
soffice = shutil.which("soffice") or shutil.which("libreoffice")
|
|
107
|
+
if not soffice:
|
|
108
|
+
sys.exit(
|
|
109
|
+
"ERROR: --pdf-out requested but no `soffice` / `libreoffice` binary found on PATH. "
|
|
110
|
+
"Install LibreOffice, or run this script inside a container that has it. "
|
|
111
|
+
"Refusing to silently skip the PDF - report this to the user."
|
|
112
|
+
)
|
|
113
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
114
|
+
result = subprocess.run(
|
|
115
|
+
[soffice, "--headless", "--convert-to", "pdf", "--outdir", tmp, str(src)],
|
|
116
|
+
capture_output=True, text=True, timeout=timeout,
|
|
117
|
+
)
|
|
118
|
+
produced = Path(tmp) / (src.stem + ".pdf")
|
|
119
|
+
if result.returncode != 0 or not produced.exists():
|
|
120
|
+
sys.exit(
|
|
121
|
+
f"ERROR: soffice failed to render PDF.\nstdout: {result.stdout}\nstderr: {result.stderr}"
|
|
122
|
+
)
|
|
123
|
+
pdf_out.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
shutil.move(str(produced), str(pdf_out))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def main() -> int:
|
|
128
|
+
p = argparse.ArgumentParser(description="Extract text, tables, notes, OCR, embeds, and (optionally) a sibling PDF from a .pptx file.")
|
|
129
|
+
p.add_argument("pptx", type=Path, help="Path to the source .pptx file.")
|
|
130
|
+
p.add_argument("--out-md", type=Path, required=True, help="Output markdown path.")
|
|
131
|
+
p.add_argument("--pdf-out", type=Path, default=None, help="Also render a sibling PDF at this path (requires soffice/LibreOffice).")
|
|
132
|
+
p.add_argument("--embeds-dir", type=Path, default=None, help="Directory to extract embedded OLE/package objects into. Skipped if none are found.")
|
|
133
|
+
p.add_argument("--no-ocr", action="store_true", help="Skip OCR of embedded images (default: OCR is attempted; missing tesseract is a hard error, not a silent skip).")
|
|
134
|
+
p.add_argument("--timeout", type=int, default=180, help="Subprocess timeout in seconds for the PDF render step (default 180).")
|
|
135
|
+
args = p.parse_args()
|
|
136
|
+
|
|
137
|
+
if not args.pptx.exists():
|
|
138
|
+
sys.exit(f"ERROR: source file does not exist: {args.pptx}")
|
|
139
|
+
|
|
140
|
+
if not args.no_ocr:
|
|
141
|
+
try:
|
|
142
|
+
import pytesseract # noqa: F401
|
|
143
|
+
from PIL import Image # noqa: F401
|
|
144
|
+
except ImportError as e:
|
|
145
|
+
sys.exit(f"ERROR: OCR requested (default) but a dependency is missing ({e}). "
|
|
146
|
+
"Install pillow + pytesseract, or pass --no-ocr to skip OCR explicitly.")
|
|
147
|
+
if not shutil.which("tesseract"):
|
|
148
|
+
sys.exit("ERROR: OCR requested (default) but the `tesseract` binary is not on PATH. "
|
|
149
|
+
"Install tesseract-ocr, or pass --no-ocr to skip OCR explicitly.")
|
|
150
|
+
|
|
151
|
+
prs = Presentation(str(args.pptx))
|
|
152
|
+
|
|
153
|
+
out_lines: list[str] = [f"# {args.pptx.name}", ""]
|
|
154
|
+
total_embeds: list[tuple[int, str, Path]] = []
|
|
155
|
+
|
|
156
|
+
for idx, slide in enumerate(prs.slides, start=1):
|
|
157
|
+
out_lines.append(f"## Slide {idx}")
|
|
158
|
+
out_lines.append("")
|
|
159
|
+
|
|
160
|
+
body_lines = shape_text_all(slide)
|
|
161
|
+
if body_lines:
|
|
162
|
+
out_lines.extend(body_lines)
|
|
163
|
+
else:
|
|
164
|
+
out_lines.append("*(no extractable text on this slide)*")
|
|
165
|
+
out_lines.append("")
|
|
166
|
+
|
|
167
|
+
if not args.no_ocr:
|
|
168
|
+
images = extract_slide_images(slide)
|
|
169
|
+
for shape_name, blob, ext in images:
|
|
170
|
+
text = ocr_image(blob)
|
|
171
|
+
if text:
|
|
172
|
+
out_lines.append(f"**OCR text from image `{shape_name}` (slide {idx}):**")
|
|
173
|
+
out_lines.append("")
|
|
174
|
+
out_lines.append(text)
|
|
175
|
+
out_lines.append("")
|
|
176
|
+
|
|
177
|
+
if slide.has_notes_slide:
|
|
178
|
+
notes = slide.notes_slide.notes_text_frame.text.strip()
|
|
179
|
+
if notes:
|
|
180
|
+
out_lines.append(f"**Speaker notes (slide {idx}):**")
|
|
181
|
+
out_lines.append("")
|
|
182
|
+
out_lines.append(notes)
|
|
183
|
+
out_lines.append("")
|
|
184
|
+
|
|
185
|
+
embeds = extract_slide_embeds(slide)
|
|
186
|
+
for fname, blob in embeds:
|
|
187
|
+
if args.embeds_dir:
|
|
188
|
+
dest = args.embeds_dir / f"slide{idx:03d}-{fname}"
|
|
189
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
dest.write_bytes(blob)
|
|
191
|
+
total_embeds.append((idx, fname, dest))
|
|
192
|
+
|
|
193
|
+
if total_embeds:
|
|
194
|
+
out_lines.append("## Embedded file objects")
|
|
195
|
+
out_lines.append("")
|
|
196
|
+
for idx, fname, dest in total_embeds:
|
|
197
|
+
out_lines.append(f"- Slide {idx}: `{fname}` -> `{dest}`")
|
|
198
|
+
out_lines.append("")
|
|
199
|
+
|
|
200
|
+
args.out_md.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
args.out_md.write_text("\n".join(out_lines), encoding="utf-8")
|
|
202
|
+
print(f"OK: wrote {args.out_md} ({len(prs.slides)} slides, {len(total_embeds)} embed(s))")
|
|
203
|
+
|
|
204
|
+
if args.pdf_out:
|
|
205
|
+
render_pdf(args.pptx, args.pdf_out, args.timeout)
|
|
206
|
+
print(f"OK: wrote {args.pdf_out}")
|
|
207
|
+
|
|
208
|
+
return 0
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def shape_text_all(slide) -> list[str]:
|
|
212
|
+
lines: list[str] = []
|
|
213
|
+
for shape in slide.shapes:
|
|
214
|
+
lines.extend(shape_text(shape))
|
|
215
|
+
return lines
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
sys.exit(main())
|