studyctl 2.4.0__tar.gz → 2.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {studyctl-2.4.0 → studyctl-2.5.0}/PKG-INFO +1 -1
- {studyctl-2.4.0 → studyctl-2.5.0}/pyproject.toml +1 -1
- studyctl-2.5.0/src/studyctl/adapters/codex.py +39 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/agent_launcher.py +3 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/__init__.py +2 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_doctor.py +88 -2
- studyctl-2.5.0/src/studyctl/cli/_install.py +80 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_session.py +9 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_setup.py +2 -2
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_shared.py +17 -20
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_study.py +1 -1
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_upgrade.py +31 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/doctor/agents.py +6 -1
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/doctor/core.py +2 -2
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/history/__init__.py +2 -0
- studyctl-2.5.0/src/studyctl/history/_connection.py +69 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/history/sessions.py +35 -0
- studyctl-2.5.0/src/studyctl/installers.py +301 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/review_db.py +4 -4
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/session/orchestrator.py +5 -1
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/session/start.py +186 -111
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/session_state.py +13 -4
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/settings.py +11 -3
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_adapter_builtins.py +26 -3
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_agent_launcher.py +25 -2
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_cli_doctor.py +14 -0
- studyctl-2.5.0/tests/test_cli_install.py +53 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_cli_session.py +18 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_cli_upgrade.py +24 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_doctor_agents.py +18 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_doctor_core.py +2 -2
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_harness_matrix.py +2 -2
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_review_db.py +36 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_session_start.py +91 -1
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_settings_custom.py +15 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_setup_wizard.py +14 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_study_integration.py +14 -1
- studyctl-2.4.0/src/studyctl/history/_connection.py +0 -55
- {studyctl-2.4.0 → studyctl-2.5.0}/.gitignore +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/README.md +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/_custom.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/_local_llm.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/_protocol.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/_strategies.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/claude.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/gemini.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/kiro.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/lmstudio.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/ollama.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/opencode.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/adapters/registry.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/__main__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_backup.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_clean.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_config.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_content.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_lazy.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_review.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_sync.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_topics.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/cli/_web.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/content/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/content/markdown_converter.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/content/models.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/content/notebooklm_client.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/content/splitter.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/content/storage.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/content/syllabus.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/data/tmux-studyctl.conf +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/db.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/doctor/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/doctor/config.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/doctor/database.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/doctor/deps.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/doctor/models.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/doctor/updates.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/history/bridges.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/history/concepts.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/history/medication.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/history/progress.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/history/search.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/history/streaks.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/history/teachback.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/logic/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/logic/backlog_logic.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/logic/break_logic.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/logic/briefing_logic.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/logic/clean_logic.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/logic/streaks_logic.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/logic/topic_resolver.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/maintenance.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/mcp/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/mcp/server.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/mcp/tools.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/output.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/parking.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/pdf.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/review_loader.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/services/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/services/backlog.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/services/content.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/services/flashcard_writer.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/services/review.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/session/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/session/cleanup.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/session/resume.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/shared.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/state.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/sync.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/tmux.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/topics.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/tui/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/tui/__main__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/tui/sidebar.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/app.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/auth.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/routes/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/routes/artefacts.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/routes/cards.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/routes/courses.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/routes/history.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/routes/session.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/routes/terminal_proxy.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/components.js +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/icon-192.svg +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/icon-512.svg +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/index.html +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/manifest.json +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/style.css +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/sw.js +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/vendor/css/files/inter-latin-ext.woff2 +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/vendor/css/files/inter-latin.woff2 +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/vendor/css/files/opendyslexic-latin-400-normal.woff +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/vendor/css/files/opendyslexic-latin-400-normal.woff2 +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/vendor/css/inter.css +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/vendor/css/opendyslexic-400.css +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/vendor/js/alpine-3.14.8.min.js +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/vendor/js/htmx-2.0.4.min.js +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/src/studyctl/web/static/vendor/js/htmx-ext-sse-2.2.2.js +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/_helpers.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/harness/__init__.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/harness/agents.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/harness/study.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/harness/terminal.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/harness/tmux.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_adapter_custom.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_adapter_protocol.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_adapter_registry.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_adapter_strategies.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_backlog_logic.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_backup.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_break_logic.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_briefing_logic.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_clean.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_cli.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_content_notebooklm.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_content_splitter.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_content_storage.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_content_syllabus.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_doctor_config.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_doctor_database.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_doctor_deps.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_doctor_integration.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_doctor_models.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_doctor_updates.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_e2e_session_demo.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_flashcard_writer.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_history.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_install_mentor_prompt.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_lan_auth.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_mcp_tools.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_orchestrator.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_parking.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_review_loader.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_session_cleanup.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_session_db_integration.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_session_state.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_shared.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_sidebar_pilot.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_streaks_logic.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_study.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_study_lifecycle.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_terminal_proxy.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_tmux.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_topic_resolver.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_topics_cli.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_uat_terminal.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_web_app.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_web_artefacts.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_web_session.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_web_terminal.py +0 -0
- {studyctl-2.4.0 → studyctl-2.5.0}/tests/test_web_vendor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: studyctl
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
4
4
|
Summary: AuDHD-aware study tool with AI Socratic mentoring, spaced repetition, and content pipeline
|
|
5
5
|
Project-URL: Homepage, https://github.com/NetDevAutomate/socratic-study-mentor
|
|
6
6
|
Project-URL: Repository, https://github.com/NetDevAutomate/socratic-study-mentor
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Codex adapter — persona via AGENTS.md in session CWD.
|
|
2
|
+
|
|
3
|
+
Codex CLI auto-loads AGENTS.md from the current working directory.
|
|
4
|
+
The setup function writes the canonical persona as plain markdown in
|
|
5
|
+
the session directory; launch just invokes the codex binary there.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from studyctl.adapters._protocol import AgentAdapter
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _codex_setup(canonical_content: str, session_dir: Path) -> Path:
|
|
20
|
+
"""Write AGENTS.md to the session dir for Codex auto-discovery."""
|
|
21
|
+
persona_path = session_dir / "AGENTS.md"
|
|
22
|
+
persona_path.write_text(canonical_content)
|
|
23
|
+
return persona_path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _codex_launch(_persona_path: Path, resume: bool) -> str:
|
|
27
|
+
"""Build Codex launch command. Codex reads AGENTS.md from cwd."""
|
|
28
|
+
binary = shutil.which("codex") or "codex"
|
|
29
|
+
if resume:
|
|
30
|
+
return f"{binary} --resume"
|
|
31
|
+
return binary
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ADAPTER = AgentAdapter(
|
|
35
|
+
name="codex",
|
|
36
|
+
binary="codex",
|
|
37
|
+
setup=_codex_setup,
|
|
38
|
+
launch_cmd=_codex_launch,
|
|
39
|
+
)
|
|
@@ -24,6 +24,7 @@ from studyctl.adapters._local_llm import _get_local_llm_config, _local_llm_env_p
|
|
|
24
24
|
from studyctl.adapters._protocol import AgentAdapter
|
|
25
25
|
from studyctl.adapters._strategies import cli_flag_setup as _claude_setup
|
|
26
26
|
from studyctl.adapters.claude import _claude_launch
|
|
27
|
+
from studyctl.adapters.codex import _codex_launch, _codex_setup
|
|
27
28
|
from studyctl.adapters.gemini import _gemini_launch, _gemini_setup
|
|
28
29
|
from studyctl.adapters.kiro import _KIRO_BACKUP_SUFFIX, KIRO_AGENT_NAME
|
|
29
30
|
from studyctl.adapters.lmstudio import _lmstudio_launch
|
|
@@ -42,6 +43,8 @@ __all__ = [
|
|
|
42
43
|
"AgentAdapter",
|
|
43
44
|
"_claude_launch",
|
|
44
45
|
"_claude_setup",
|
|
46
|
+
"_codex_launch",
|
|
47
|
+
"_codex_setup",
|
|
45
48
|
"_gemini_launch",
|
|
46
49
|
"_gemini_mcp",
|
|
47
50
|
"_gemini_setup",
|
|
@@ -22,6 +22,8 @@ from studyctl.cli._lazy import LazyGroup
|
|
|
22
22
|
"dedup": "studyctl.cli._sync:dedup",
|
|
23
23
|
# _setup.py — first-run setup wizard
|
|
24
24
|
"setup": "studyctl.cli._setup:setup",
|
|
25
|
+
# _install.py — typed installation helpers
|
|
26
|
+
"install": "studyctl.cli._install:install_group",
|
|
25
27
|
# _config.py — configuration
|
|
26
28
|
"config": "studyctl.cli._config:config_group",
|
|
27
29
|
# _review.py — spaced repetition, progress, wins, streaks, bridges
|
|
@@ -9,6 +9,15 @@ from rich.table import Table
|
|
|
9
9
|
|
|
10
10
|
from studyctl.cli._shared import console
|
|
11
11
|
from studyctl.doctor.models import VALID_CATEGORIES, CheckResult
|
|
12
|
+
from studyctl.installers import (
|
|
13
|
+
InstallError,
|
|
14
|
+
ensure_default_config,
|
|
15
|
+
ensure_review_database,
|
|
16
|
+
ensure_review_directories,
|
|
17
|
+
install_agent_definitions,
|
|
18
|
+
install_workspace_tools,
|
|
19
|
+
require_repo_root,
|
|
20
|
+
)
|
|
12
21
|
|
|
13
22
|
|
|
14
23
|
def _get_registry():
|
|
@@ -94,13 +103,70 @@ def _summary_line(results: list[CheckResult]) -> str:
|
|
|
94
103
|
parts.append(f"{counts['info']} info")
|
|
95
104
|
summary = ", ".join(parts) + "."
|
|
96
105
|
if auto_fixable:
|
|
97
|
-
summary += f" Run 'studyctl
|
|
106
|
+
summary += f" Run 'studyctl doctor --fix' to fix {auto_fixable} issues."
|
|
98
107
|
return summary
|
|
99
108
|
|
|
100
109
|
|
|
110
|
+
def _apply_fixes(results: list[CheckResult]) -> list[str]:
|
|
111
|
+
"""Apply safe automatic fixes for the provided results."""
|
|
112
|
+
actions: list[str] = []
|
|
113
|
+
|
|
114
|
+
def needs(category: str, name: str | None = None) -> bool:
|
|
115
|
+
return any(
|
|
116
|
+
r.category == category
|
|
117
|
+
and (name is None or r.name == name)
|
|
118
|
+
and r.status in ("warn", "fail")
|
|
119
|
+
and r.fix_auto
|
|
120
|
+
for r in results
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if needs("core", "config_file"):
|
|
124
|
+
path = ensure_default_config()
|
|
125
|
+
actions.append(f"created config: {path}")
|
|
126
|
+
|
|
127
|
+
if needs("core", "agent_session_tools"):
|
|
128
|
+
repo_root = require_repo_root()
|
|
129
|
+
install_workspace_tools(repo_root, sync_workspace=True, force=True)
|
|
130
|
+
actions.append("reinstalled workspace tools")
|
|
131
|
+
|
|
132
|
+
if any(
|
|
133
|
+
r.category == "config"
|
|
134
|
+
and r.name.startswith("review_dir_")
|
|
135
|
+
and r.status in ("warn", "fail")
|
|
136
|
+
and r.fix_auto
|
|
137
|
+
for r in results
|
|
138
|
+
):
|
|
139
|
+
created = ensure_review_directories()
|
|
140
|
+
actions.append(f"ensured review directories ({len(created)} created)")
|
|
141
|
+
|
|
142
|
+
if needs("database", "review_db"):
|
|
143
|
+
db_path = ensure_review_database()
|
|
144
|
+
actions.append(f"migrated review DB: {db_path}")
|
|
145
|
+
|
|
146
|
+
if any(r.category == "agents" and r.status in ("warn", "fail") and r.fix_auto for r in results):
|
|
147
|
+
repo_root = require_repo_root()
|
|
148
|
+
summary = install_agent_definitions(repo_root)
|
|
149
|
+
changed = sum(summary.values())
|
|
150
|
+
actions.append(f"refreshed agent definitions ({changed} changes)")
|
|
151
|
+
|
|
152
|
+
if any(
|
|
153
|
+
r.category == "updates" and r.status in ("warn", "fail") and r.fix_auto for r in results
|
|
154
|
+
):
|
|
155
|
+
from studyctl.cli._upgrade import _detect_package_manager, _upgrade_packages
|
|
156
|
+
|
|
157
|
+
manager = _detect_package_manager()
|
|
158
|
+
if not _upgrade_packages(manager, dry_run=False):
|
|
159
|
+
msg = "package upgrade failed"
|
|
160
|
+
raise InstallError(msg)
|
|
161
|
+
actions.append(f"upgraded packages via {manager}")
|
|
162
|
+
|
|
163
|
+
return actions
|
|
164
|
+
|
|
165
|
+
|
|
101
166
|
@click.command("doctor")
|
|
102
167
|
@click.option("--json", "as_json", is_flag=True, help="Output as JSON array")
|
|
103
168
|
@click.option("--quiet", is_flag=True, help="Summary line only")
|
|
169
|
+
@click.option("--fix", is_flag=True, help="Apply safe automatic fixes before reporting.")
|
|
104
170
|
@click.option(
|
|
105
171
|
"--category",
|
|
106
172
|
type=click.Choice(sorted(VALID_CATEGORIES)),
|
|
@@ -108,11 +174,25 @@ def _summary_line(results: list[CheckResult]) -> str:
|
|
|
108
174
|
help="Check specific category",
|
|
109
175
|
)
|
|
110
176
|
@click.pass_context
|
|
111
|
-
def doctor(
|
|
177
|
+
def doctor(
|
|
178
|
+
ctx: click.Context,
|
|
179
|
+
as_json: bool,
|
|
180
|
+
quiet: bool,
|
|
181
|
+
fix: bool,
|
|
182
|
+
category: str | None,
|
|
183
|
+
) -> None:
|
|
112
184
|
"""Check installation health and report issues."""
|
|
113
185
|
registry = _get_registry()
|
|
114
186
|
|
|
115
187
|
results = registry.run_category(category) if category else registry.run_all()
|
|
188
|
+
applied: list[str] = []
|
|
189
|
+
if fix:
|
|
190
|
+
try:
|
|
191
|
+
applied = _apply_fixes(results)
|
|
192
|
+
except (InstallError, click.ClickException) as exc:
|
|
193
|
+
raise click.ClickException(str(exc)) from exc
|
|
194
|
+
if applied:
|
|
195
|
+
results = registry.run_category(category) if category else registry.run_all()
|
|
116
196
|
|
|
117
197
|
exit_code = _compute_exit_code(results)
|
|
118
198
|
|
|
@@ -127,6 +207,12 @@ def doctor(ctx: click.Context, as_json: bool, quiet: bool, category: str | None)
|
|
|
127
207
|
return
|
|
128
208
|
|
|
129
209
|
# Rich table output grouped by category
|
|
210
|
+
if applied:
|
|
211
|
+
console.print("[bold green]Applied fixes:[/bold green]")
|
|
212
|
+
for action in applied:
|
|
213
|
+
console.print(f" {action}")
|
|
214
|
+
console.print()
|
|
215
|
+
|
|
130
216
|
table = Table(title="studyctl doctor", show_lines=False)
|
|
131
217
|
table.add_column("Status", justify="center", width=3)
|
|
132
218
|
table.add_column("Check", style="cyan")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Typed install commands for tools and agent definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from studyctl.cli._shared import console
|
|
10
|
+
from studyctl.installers import (
|
|
11
|
+
InstallError,
|
|
12
|
+
install_agent_definitions,
|
|
13
|
+
install_workspace_tools,
|
|
14
|
+
require_repo_root,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group(name="install")
|
|
19
|
+
def install_group() -> None:
|
|
20
|
+
"""Install studyctl tools and agent definitions."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@install_group.command(name="tools")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--repo-root",
|
|
26
|
+
type=click.Path(path_type=Path, file_okay=False, dir_okay=True),
|
|
27
|
+
default=None,
|
|
28
|
+
help="Repository root to install from (defaults to auto-detect).",
|
|
29
|
+
)
|
|
30
|
+
@click.option("--sync/--skip-sync", default=True, show_default=True, help="Run `uv sync` first.")
|
|
31
|
+
@click.option(
|
|
32
|
+
"--force/--no-force",
|
|
33
|
+
default=True,
|
|
34
|
+
show_default=True,
|
|
35
|
+
help="Force reinstall uv tools.",
|
|
36
|
+
)
|
|
37
|
+
def install_tools(repo_root: Path | None, sync: bool, force: bool) -> None:
|
|
38
|
+
"""Install editable workspace packages as uv tools."""
|
|
39
|
+
try:
|
|
40
|
+
root = require_repo_root(repo_root)
|
|
41
|
+
installed = install_workspace_tools(root, sync_workspace=sync, force=force)
|
|
42
|
+
except InstallError as exc:
|
|
43
|
+
raise click.ClickException(str(exc)) from exc
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
raise click.ClickException(f"Tool installation failed: {exc}") from exc
|
|
46
|
+
|
|
47
|
+
console.print("[green]Installed tools:[/green] " + ", ".join(installed))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@install_group.command(name="agents")
|
|
51
|
+
@click.option(
|
|
52
|
+
"--repo-root",
|
|
53
|
+
type=click.Path(path_type=Path, file_okay=False, dir_okay=True),
|
|
54
|
+
default=None,
|
|
55
|
+
help="Repository root to install from (defaults to auto-detect).",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--tool",
|
|
59
|
+
"tools",
|
|
60
|
+
multiple=True,
|
|
61
|
+
type=click.Choice(["kiro", "claude", "gemini", "opencode", "codex", "amp"]),
|
|
62
|
+
help="Install for a specific AI tool. Repeat to install multiple.",
|
|
63
|
+
)
|
|
64
|
+
@click.option("--uninstall", is_flag=True, help="Remove installed agent definitions instead.")
|
|
65
|
+
def install_agents(repo_root: Path | None, tools: tuple[str, ...], uninstall: bool) -> None:
|
|
66
|
+
"""Install or remove agent definitions for supported AI tools."""
|
|
67
|
+
try:
|
|
68
|
+
root = require_repo_root(repo_root)
|
|
69
|
+
summary = install_agent_definitions(root, tools=list(tools) or None, uninstall=uninstall)
|
|
70
|
+
except InstallError as exc:
|
|
71
|
+
raise click.ClickException(str(exc)) from exc
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
action = "Agent uninstall" if uninstall else "Agent install"
|
|
74
|
+
raise click.ClickException(f"{action} failed: {exc}") from exc
|
|
75
|
+
|
|
76
|
+
action_word = "Removed" if uninstall else "Updated"
|
|
77
|
+
lines = [f"{name}: {count}" for name, count in summary.items()]
|
|
78
|
+
console.print(f"[green]{action_word} agent definitions.[/green]")
|
|
79
|
+
for line in lines:
|
|
80
|
+
console.print(f" {line}")
|
|
@@ -29,9 +29,18 @@ def session_start(topic: str, energy: int) -> None:
|
|
|
29
29
|
PARKING_FILE,
|
|
30
30
|
TOPICS_FILE,
|
|
31
31
|
_ensure_session_dir,
|
|
32
|
+
is_session_active,
|
|
32
33
|
write_session_state,
|
|
33
34
|
)
|
|
34
35
|
|
|
36
|
+
if is_session_active():
|
|
37
|
+
console.print(
|
|
38
|
+
"[yellow]A session is already active.[/yellow]\n"
|
|
39
|
+
" Check: [bold]studyctl session status[/bold]\n"
|
|
40
|
+
" End: [bold]studyctl session end[/bold]"
|
|
41
|
+
)
|
|
42
|
+
raise SystemExit(1)
|
|
43
|
+
|
|
35
44
|
_ensure_session_dir()
|
|
36
45
|
|
|
37
46
|
from studyctl.output import energy_to_label
|
|
@@ -58,14 +58,14 @@ def setup() -> None:
|
|
|
58
58
|
# Step 2 — AI coding assistant / MCP registration
|
|
59
59
|
# ------------------------------------------------------------------
|
|
60
60
|
console.print("[bold]Step 2 of 5[/bold] Do you use an AI coding assistant?")
|
|
61
|
-
console.print(" [dim](Claude Code, Kiro, Gemini CLI, etc.)[/dim]")
|
|
61
|
+
console.print(" [dim](Claude Code, Codex CLI, Kiro, Gemini CLI, etc.)[/dim]")
|
|
62
62
|
has_ai = click.confirm(" Use an AI assistant?", default=True)
|
|
63
63
|
if has_ai:
|
|
64
64
|
assistant = click.prompt(
|
|
65
65
|
" Which one",
|
|
66
66
|
default="claude-code",
|
|
67
67
|
type=click.Choice(
|
|
68
|
-
["claude-code", "kiro", "gemini-cli", "other"],
|
|
68
|
+
["claude-code", "codex", "kiro", "gemini-cli", "other"],
|
|
69
69
|
case_sensitive=False,
|
|
70
70
|
),
|
|
71
71
|
show_choices=True,
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import subprocess
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
|
|
7
|
+
from click import ClickException
|
|
8
|
+
|
|
9
|
+
from studyctl.installers import InstallError, install_agent_definitions, require_repo_root
|
|
8
10
|
from studyctl.output import console
|
|
9
11
|
from studyctl.topics import Topic, get_topics
|
|
10
12
|
|
|
@@ -50,31 +52,26 @@ def offer_agent_install(flag: bool | None) -> None:
|
|
|
50
52
|
Args:
|
|
51
53
|
flag: True = install, False = skip, None = ask interactively.
|
|
52
54
|
"""
|
|
53
|
-
# Find install-agents.sh relative to the package
|
|
54
|
-
candidate = Path(__file__).resolve().parent.parent
|
|
55
|
-
for _ in range(6):
|
|
56
|
-
script = candidate / "scripts" / "install-agents.sh"
|
|
57
|
-
if script.exists():
|
|
58
|
-
break
|
|
59
|
-
candidate = candidate.parent
|
|
60
|
-
else:
|
|
61
|
-
return # Script not found — skip silently (pip install, not git clone)
|
|
62
|
-
|
|
63
55
|
if flag is None:
|
|
64
56
|
console.print("\n[bold cyan]Agent Installation[/bold cyan]")
|
|
65
57
|
console.print(
|
|
66
58
|
"The study mentor agents can be installed for detected AI tools\n"
|
|
67
|
-
"(Claude Code, Kiro CLI, Gemini, OpenCode, Amp).\n"
|
|
59
|
+
"(Claude Code, Codex CLI, Kiro CLI, Gemini, OpenCode, Amp).\n"
|
|
68
60
|
)
|
|
69
61
|
reply = input("Install agent definitions now? [Y/n] ").strip().lower()
|
|
70
62
|
flag = reply in ("", "y", "yes")
|
|
71
63
|
|
|
72
64
|
if flag:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
console.print(f"
|
|
65
|
+
try:
|
|
66
|
+
repo_root = require_repo_root(Path.cwd())
|
|
67
|
+
summary = install_agent_definitions(repo_root)
|
|
68
|
+
except (InstallError, OSError) as exc:
|
|
69
|
+
console.print(f"[yellow]Agent install skipped:[/yellow] {exc}")
|
|
70
|
+
return
|
|
71
|
+
except ClickException as exc:
|
|
72
|
+
console.print(f"[yellow]Agent install skipped:[/yellow] {exc}")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
console.print("[green]Agent definitions installed.[/green]")
|
|
76
|
+
for name, count in summary.items():
|
|
77
|
+
console.print(f" {name}: {count}")
|
|
@@ -242,7 +242,7 @@ def sidebar_cmd() -> None:
|
|
|
242
242
|
except ImportError:
|
|
243
243
|
console.print(
|
|
244
244
|
"[red]Textual is required for the sidebar.[/red]\n"
|
|
245
|
-
" Install:
|
|
245
|
+
" Install: pip install 'studyctl[tui]'"
|
|
246
246
|
)
|
|
247
247
|
raise SystemExit(1) from None
|
|
248
248
|
|
|
@@ -15,6 +15,7 @@ from rich.table import Table
|
|
|
15
15
|
|
|
16
16
|
from studyctl.cli._doctor import _get_registry
|
|
17
17
|
from studyctl.cli._shared import console
|
|
18
|
+
from studyctl.installers import InstallError, install_agent_definitions, require_repo_root
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from studyctl.doctor.models import CheckResult
|
|
@@ -171,6 +172,28 @@ def _upgrade_database(dry_run: bool) -> bool:
|
|
|
171
172
|
return True
|
|
172
173
|
|
|
173
174
|
|
|
175
|
+
def _upgrade_agents(dry_run: bool) -> bool:
|
|
176
|
+
"""Install or refresh agent definitions from the current source checkout."""
|
|
177
|
+
try:
|
|
178
|
+
repo_root = require_repo_root()
|
|
179
|
+
except InstallError as exc:
|
|
180
|
+
console.print(f"[red] Agent upgrade failed:[/red] {exc}")
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
if dry_run:
|
|
184
|
+
console.print(f" [dim]Would refresh agent definitions from {repo_root}[/dim]")
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
install_agent_definitions(repo_root)
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
console.print(f"[red] Agent upgrade failed:[/red] {exc}")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
console.print("[green] Agent definitions refreshed.[/green]")
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
|
|
174
197
|
def _result_matches_component(result: CheckResult, component: str) -> bool:
|
|
175
198
|
"""Return True if *result* is relevant to the upgrade *component*."""
|
|
176
199
|
if component == "all":
|
|
@@ -263,12 +286,15 @@ def upgrade(ctx: click.Context, dry_run: bool, component: str, force: bool) -> N
|
|
|
263
286
|
_upgrade_packages(manager, dry_run=True)
|
|
264
287
|
elif mapped == "database":
|
|
265
288
|
_upgrade_database(dry_run=True)
|
|
289
|
+
elif mapped == "agents":
|
|
290
|
+
_upgrade_agents(dry_run=True)
|
|
266
291
|
ctx.exit(0)
|
|
267
292
|
return
|
|
268
293
|
|
|
269
294
|
# Non-dry-run: apply upgrades grouped by component
|
|
270
295
|
needs_packages = any(_CATEGORY_TO_COMPONENT.get(r.category) == "packages" for r in actionable)
|
|
271
296
|
needs_database = any(_CATEGORY_TO_COMPONENT.get(r.category) == "database" for r in actionable)
|
|
297
|
+
needs_agents = any(_CATEGORY_TO_COMPONENT.get(r.category) == "agents" for r in actionable)
|
|
272
298
|
|
|
273
299
|
success = True
|
|
274
300
|
|
|
@@ -283,6 +309,11 @@ def upgrade(ctx: click.Context, dry_run: bool, component: str, force: bool) -> N
|
|
|
283
309
|
if not _upgrade_database(dry_run=False):
|
|
284
310
|
success = False
|
|
285
311
|
|
|
312
|
+
if needs_agents:
|
|
313
|
+
console.print("[bold]Upgrading agent definitions...[/bold]")
|
|
314
|
+
if not _upgrade_agents(dry_run=False):
|
|
315
|
+
success = False
|
|
316
|
+
|
|
286
317
|
if success:
|
|
287
318
|
console.print("\n[green]Upgrade complete.[/green]")
|
|
288
319
|
else:
|
|
@@ -16,11 +16,13 @@ import urllib.request
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
from studyctl.doctor.models import CheckResult
|
|
19
|
+
from studyctl.installers import find_repo_root
|
|
19
20
|
|
|
20
21
|
MANIFEST_URL = "https://raw.githubusercontent.com/NetDevAutomate/socratic-study-mentor/main/agents/manifest.json"
|
|
21
22
|
|
|
22
23
|
TOOL_AGENTS: dict[str, tuple[str, str]] = {
|
|
23
24
|
"claude": ("claude", "~/.claude/commands/socratic-mentor.md"),
|
|
25
|
+
"codex": ("codex", "{repo_root}/AGENTS.md"),
|
|
24
26
|
"kiro": ("kiro-cli", "~/.kiro/agents/study-mentor.json"),
|
|
25
27
|
"gemini": ("gemini", "~/.gemini/agents/study-mentor.md"),
|
|
26
28
|
"opencode": ("opencode", "~/.config/opencode/agents/study-mentor.md"),
|
|
@@ -35,6 +37,9 @@ def _detect_ai_tools() -> list[str]:
|
|
|
35
37
|
|
|
36
38
|
def _get_agent_install_path(tool: str) -> Path:
|
|
37
39
|
_, path_template = TOOL_AGENTS[tool]
|
|
40
|
+
if "{repo_root}" in path_template:
|
|
41
|
+
repo_root = find_repo_root(Path.cwd()) or Path.cwd()
|
|
42
|
+
return Path(path_template.format(repo_root=repo_root)).expanduser()
|
|
38
43
|
return Path(path_template).expanduser()
|
|
39
44
|
|
|
40
45
|
|
|
@@ -230,7 +235,7 @@ def check_agent_definitions() -> list[CheckResult]:
|
|
|
230
235
|
"no_ai_tools",
|
|
231
236
|
"info",
|
|
232
237
|
"No AI coding tools detected",
|
|
233
|
-
"Install Claude Code, Kiro CLI, Gemini CLI, or
|
|
238
|
+
"Install Claude Code, Codex CLI, Kiro CLI, Gemini CLI, OpenCode, or Amp",
|
|
234
239
|
False,
|
|
235
240
|
)
|
|
236
241
|
]
|
|
@@ -68,7 +68,7 @@ def check_agent_session_tools() -> list[CheckResult]:
|
|
|
68
68
|
"agent_session_tools",
|
|
69
69
|
"warn",
|
|
70
70
|
"agent-session-tools not installed (sessions DB unavailable)",
|
|
71
|
-
"
|
|
71
|
+
"studyctl install tools",
|
|
72
72
|
fix_auto=True,
|
|
73
73
|
)
|
|
74
74
|
]
|
|
@@ -101,7 +101,7 @@ def check_config_file() -> list[CheckResult]:
|
|
|
101
101
|
"config_file",
|
|
102
102
|
"warn",
|
|
103
103
|
f"Config not found: {config_path}",
|
|
104
|
-
"studyctl
|
|
104
|
+
"studyctl doctor --fix",
|
|
105
105
|
fix_auto=True,
|
|
106
106
|
)
|
|
107
107
|
]
|
|
@@ -18,6 +18,7 @@ from .progress import (
|
|
|
18
18
|
)
|
|
19
19
|
from .search import struggle_topics, topic_frequency
|
|
20
20
|
from .sessions import (
|
|
21
|
+
abort_study_session,
|
|
21
22
|
end_study_session,
|
|
22
23
|
get_energy_session_data,
|
|
23
24
|
get_last_session_summary,
|
|
@@ -30,6 +31,7 @@ from .teachback import get_teachback_history, record_teachback
|
|
|
30
31
|
|
|
31
32
|
__all__ = [
|
|
32
33
|
"ConceptSummary",
|
|
34
|
+
"abort_study_session",
|
|
33
35
|
"check_medication_window",
|
|
34
36
|
"end_study_session",
|
|
35
37
|
"get_bridges",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shared database connection helpers for the history package.
|
|
2
|
+
|
|
3
|
+
Auto-creates the sessions DB and applies migrations on first use,
|
|
4
|
+
so ``studyctl study`` works on a fresh machine without ``studyctl doctor``
|
|
5
|
+
or any other bootstrap step.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from ..db import connect_db
|
|
13
|
+
from ..settings import load_settings
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_db_path():
|
|
19
|
+
"""Return the configured sessions DB path (always a Path, never None)."""
|
|
20
|
+
return load_settings().session_db
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _has_schema(conn) -> bool:
|
|
24
|
+
"""Check whether the study_sessions table exists."""
|
|
25
|
+
row = conn.execute(
|
|
26
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='study_sessions'"
|
|
27
|
+
).fetchone()
|
|
28
|
+
return row is not None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _connect():
|
|
32
|
+
"""Open a connection to sessions.db, applying schema and migrations.
|
|
33
|
+
|
|
34
|
+
On every connection: applies base schema if tables are missing, then
|
|
35
|
+
runs any pending migrations. Both operations are idempotent.
|
|
36
|
+
Returns ``None`` only if agent-session-tools is not installed or
|
|
37
|
+
schema setup fails.
|
|
38
|
+
"""
|
|
39
|
+
db = _get_db_path()
|
|
40
|
+
db.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
conn = connect_db(db, row_factory=True)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from agent_session_tools.export_sessions import SCHEMA_FILE
|
|
46
|
+
from agent_session_tools.migrations import migrate
|
|
47
|
+
except ImportError:
|
|
48
|
+
# Without agent-session-tools we can still read an existing DB
|
|
49
|
+
# but cannot create or upgrade one.
|
|
50
|
+
if _has_schema(conn):
|
|
51
|
+
return conn
|
|
52
|
+
logger.warning("agent-session-tools not installed — cannot initialise sessions DB")
|
|
53
|
+
conn.close()
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
if not _has_schema(conn):
|
|
58
|
+
with open(SCHEMA_FILE) as f:
|
|
59
|
+
conn.executescript(f.read())
|
|
60
|
+
logger.info("Created sessions DB at %s", db)
|
|
61
|
+
|
|
62
|
+
# Always run pending migrations — safe on an up-to-date DB
|
|
63
|
+
migrate(conn)
|
|
64
|
+
except Exception:
|
|
65
|
+
logger.exception("Failed to initialise/migrate sessions DB")
|
|
66
|
+
conn.close()
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
return conn
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import sqlite3
|
|
6
7
|
import uuid
|
|
7
8
|
from datetime import UTC, datetime
|
|
8
9
|
|
|
9
10
|
from . import _connection, search
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
def start_study_session(
|
|
13
16
|
topic: str,
|
|
@@ -34,6 +37,10 @@ def start_study_session(
|
|
|
34
37
|
conn.commit()
|
|
35
38
|
return study_id
|
|
36
39
|
except sqlite3.OperationalError:
|
|
40
|
+
logger.warning(
|
|
41
|
+
"Failed to insert study session — sessions DB may lack schema",
|
|
42
|
+
exc_info=True,
|
|
43
|
+
)
|
|
37
44
|
return None
|
|
38
45
|
finally:
|
|
39
46
|
conn.close()
|
|
@@ -108,6 +115,34 @@ def end_study_session(
|
|
|
108
115
|
conn.close()
|
|
109
116
|
|
|
110
117
|
|
|
118
|
+
def abort_study_session(study_id: str, reason: str) -> bool:
|
|
119
|
+
"""Mark a study session as ended when startup fails before steady state."""
|
|
120
|
+
conn = _connection._connect()
|
|
121
|
+
if not conn:
|
|
122
|
+
return False
|
|
123
|
+
try:
|
|
124
|
+
now = datetime.now(UTC).isoformat()
|
|
125
|
+
conn.execute(
|
|
126
|
+
"""
|
|
127
|
+
UPDATE study_sessions
|
|
128
|
+
SET ended_at = COALESCE(ended_at, ?),
|
|
129
|
+
duration_minutes = COALESCE(duration_minutes, 0),
|
|
130
|
+
notes = CASE
|
|
131
|
+
WHEN notes IS NULL OR notes = '' THEN ?
|
|
132
|
+
ELSE notes || char(10) || ?
|
|
133
|
+
END
|
|
134
|
+
WHERE id = ?
|
|
135
|
+
""",
|
|
136
|
+
(now, reason, reason, study_id),
|
|
137
|
+
)
|
|
138
|
+
conn.commit()
|
|
139
|
+
return True
|
|
140
|
+
except sqlite3.OperationalError:
|
|
141
|
+
return False
|
|
142
|
+
finally:
|
|
143
|
+
conn.close()
|
|
144
|
+
|
|
145
|
+
|
|
111
146
|
def get_study_session_stats(days: int = 30) -> list[dict]:
|
|
112
147
|
"""Get study session stats grouped by course slug (or raw topic) for the given period."""
|
|
113
148
|
conn = _connection._connect()
|