reprompt-cli 1.7.1__tar.gz → 1.8.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.
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/PKG-INFO +3 -2
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/README.md +2 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/pyproject.toml +1 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/__init__.py +1 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/cli.py +59 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/pipeline.py +99 -0
- reprompt_cli-1.8.0/src/reprompt/core/session_quality.py +203 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/suggestions.py +3 -0
- reprompt_cli-1.8.0/src/reprompt/output/sessions_terminal.py +172 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/storage/db.py +123 -1
- reprompt_cli-1.8.0/tests/test_db_session_quality.py +181 -0
- reprompt_cli-1.8.0/tests/test_session_quality.py +455 -0
- reprompt_cli-1.8.0/tests/test_sessions_cli.py +193 -0
- reprompt_cli-1.8.0/tests/test_sessions_output.py +102 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_suggestions.py +5 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/uv.lock +1 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.editorconfig +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/dependabot.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/workflows/ci.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/workflows/publish.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.gitignore +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.pre-commit-config.yaml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.pre-commit-hooks.yaml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.testmondata-shm +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.testmondata-wal +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/CHANGELOG.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/CODE_OF_CONDUCT.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/CONTRIBUTING.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/LICENSE +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/SECURITY.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/action.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/demo.gif +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-128.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-16.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-256.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-32.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-48.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-512.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-96.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon.svg +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-128.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-16.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-256.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-32.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-48.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-512.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-96.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon.svg +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-128.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-16.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-256.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-32.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-48.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-512.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-96.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon.svg +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-128.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-16.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-256.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-32.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-48.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-512.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-96.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon.svg +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/generate.sh +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/scripts/generate_demo_data.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/aider.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/base.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/chatgpt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/claude_chat.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/claude_code.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/cline.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/codex.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/cursor.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/filters.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/gemini.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/openclaw.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/handler.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/host.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/manifest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/protocol.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/commands/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/commands/telemetry.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/commands/wrapped.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/config.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/agent.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/analyzer.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/compress.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/conversation.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/cost.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/dashboard.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/dedup.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/digest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/distill.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/effectiveness.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/extractors.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/extractors_zh.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/insights.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/lang_detect.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/library.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/lint.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/merge_view.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/models.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/persona.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/privacy.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/privacy_scan.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/prompt_dna.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/recommend.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/scorer.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/segmenter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/session_meta.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/session_type.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/style.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/templates.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/timeutil.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/trends.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/wrapped.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/demo.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/base.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/local_embed.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/ollama.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/openai_embed.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/tfidf.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/mcp.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/mcp_main.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/agent_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/chartjs.min.js +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/compress_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/dashboard_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/distill_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/export.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/html_report.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/json_out.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/markdown.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/wrapped_html.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/wrapped_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/py.typed +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/sharing/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/sharing/client.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/sharing/clipboard.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/storage/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/collector.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/consent.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/events.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/prompt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/queue.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/sender.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/conftest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/aider_chat_history.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/chatgpt_conversations.json +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/claude_chat_export.json +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/claude_session.jsonl +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/export/default_export.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/export/full_export.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/gemini_session.json +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/openclaw_session.jsonl +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_aider.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_chatgpt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_claude.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_claude_chat.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_cline.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_gemini.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_openclaw.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_agent.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_agent_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_analyzer.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_handler.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_integration.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_manifest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_protocol.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cli_deprecations.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cli_library_effectiveness.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_clipboard.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_codex_adapter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compare_best_worst.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress_dna.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress_html.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress_insights.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_config.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_conversation.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_copy_flag.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cost.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_coverage_boost.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cursor_adapter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_dashboard.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_db.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_db_digest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_db_effectiveness.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_db_trends.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_dedup.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_demo.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_deprecated_commands.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_digest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_digest_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_distill.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_distill_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_distill_weights.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_effectiveness.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_embeddings_local.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_embeddings_ollama.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_embeddings_openai.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_empty_state.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_export.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_export_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_export_snapshot.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_extractors.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_extractors_routing.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_extractors_zh.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_extractors_zh_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_html_report.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_import_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_import_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_insights.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_insights_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_insights_expanded.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_install_hook.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_lang_detect.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_library.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_lint.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_lint_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_markdown.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_mcp.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_merge_view.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_models.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_output.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_parse_conversation_base.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_parse_conversation_chatgpt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_parse_conversation_claude.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_persona.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_pipeline.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy_output.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy_scan.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_prompt_dna.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_public_api.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_recommend.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_schema_version.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_score_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_scorer.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_segmenter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_session_type.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_share_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_sharing_client.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_source_filter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_style.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_style_trends.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_collector.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_consent.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_events.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_prompt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_queue.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_sender.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_template_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_templates.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_timeutil.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_trends.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_trends_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_use_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped_html.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped_output.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped_share.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reprompt-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: Discover, analyze, and optimize your prompts from AI coding sessions
|
|
5
5
|
Project-URL: Homepage, https://github.com/reprompt-dev/reprompt
|
|
6
6
|
Project-URL: Repository, https://github.com/reprompt-dev/reprompt
|
|
@@ -185,7 +185,7 @@ reprompt install-hook # adds post-session hook to Claude Code
|
|
|
185
185
|
|
|
186
186
|
Capture prompts from ChatGPT, Claude.ai, and Gemini directly in your browser:
|
|
187
187
|
|
|
188
|
-
1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
|
|
188
|
+
1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn) or [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
|
|
189
189
|
2. **Connect to the CLI:** `reprompt install-extension`
|
|
190
190
|
3. **Verify:** `reprompt extension-status`
|
|
191
191
|
|
|
@@ -236,6 +236,7 @@ reprompt lint --json # machine-readable output
|
|
|
236
236
|
|
|
237
237
|
- **Website:** [getreprompt.dev](https://getreprompt.dev)
|
|
238
238
|
- **Chrome Extension:** [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
|
|
239
|
+
- **Firefox Add-on:** [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
|
|
239
240
|
- **PyPI:** [reprompt-cli](https://pypi.org/project/reprompt-cli/)
|
|
240
241
|
- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
|
|
241
242
|
- **Privacy:** [getreprompt.dev/privacy](https://getreprompt.dev/privacy)
|
|
@@ -140,7 +140,7 @@ reprompt install-hook # adds post-session hook to Claude Code
|
|
|
140
140
|
|
|
141
141
|
Capture prompts from ChatGPT, Claude.ai, and Gemini directly in your browser:
|
|
142
142
|
|
|
143
|
-
1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
|
|
143
|
+
1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn) or [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
|
|
144
144
|
2. **Connect to the CLI:** `reprompt install-extension`
|
|
145
145
|
3. **Verify:** `reprompt extension-status`
|
|
146
146
|
|
|
@@ -191,6 +191,7 @@ reprompt lint --json # machine-readable output
|
|
|
191
191
|
|
|
192
192
|
- **Website:** [getreprompt.dev](https://getreprompt.dev)
|
|
193
193
|
- **Chrome Extension:** [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
|
|
194
|
+
- **Firefox Add-on:** [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
|
|
194
195
|
- **PyPI:** [reprompt-cli](https://pypi.org/project/reprompt-cli/)
|
|
195
196
|
- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
|
|
196
197
|
- **Privacy:** [getreprompt.dev/privacy](https://getreprompt.dev/privacy)
|
|
@@ -1856,7 +1856,7 @@ def _create_host_wrapper() -> Path:
|
|
|
1856
1856
|
# Find the Python executable that has reprompt installed
|
|
1857
1857
|
python_path = sys_mod.executable
|
|
1858
1858
|
|
|
1859
|
-
wrapper_path.write_text(f
|
|
1859
|
+
wrapper_path.write_text(f'#!/bin/sh\nexec "{python_path}" -u -m reprompt.bridge.host\n')
|
|
1860
1860
|
wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC)
|
|
1861
1861
|
return wrapper_path
|
|
1862
1862
|
|
|
@@ -1916,6 +1916,64 @@ def extension_status() -> None:
|
|
|
1916
1916
|
console.print(" Last sync: never")
|
|
1917
1917
|
|
|
1918
1918
|
|
|
1919
|
+
@app.command(rich_help_panel="Analyze")
|
|
1920
|
+
def sessions(
|
|
1921
|
+
last: int = typer.Option(10, "--last", help="Show N most recent sessions"),
|
|
1922
|
+
source: str = typer.Option(
|
|
1923
|
+
None, "--source", "-s", help="Filter by source (e.g. claude-code, cursor)"
|
|
1924
|
+
),
|
|
1925
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
1926
|
+
detail: str = typer.Option(None, "--detail", help="Deep-dive into a session ID"),
|
|
1927
|
+
copy: bool = typer.Option(False, "--copy", help="Copy result to clipboard"),
|
|
1928
|
+
) -> None:
|
|
1929
|
+
"""Session quality overview: composite scores, frustration signals, trends."""
|
|
1930
|
+
import json as json_mod
|
|
1931
|
+
|
|
1932
|
+
from reprompt.config import Settings
|
|
1933
|
+
from reprompt.output.sessions_terminal import render_session_detail, render_sessions_table
|
|
1934
|
+
from reprompt.storage.db import PromptDB
|
|
1935
|
+
|
|
1936
|
+
settings = Settings()
|
|
1937
|
+
db = PromptDB(settings.db_path)
|
|
1938
|
+
|
|
1939
|
+
if detail:
|
|
1940
|
+
# Single session detail view
|
|
1941
|
+
all_sessions = db.get_sessions_with_quality(limit=500)
|
|
1942
|
+
match = next((s for s in all_sessions if s.get("session_id") == detail), None)
|
|
1943
|
+
if not match:
|
|
1944
|
+
# Try prefix match
|
|
1945
|
+
match = next(
|
|
1946
|
+
(s for s in all_sessions if (s.get("session_id") or "").startswith(detail)),
|
|
1947
|
+
None,
|
|
1948
|
+
)
|
|
1949
|
+
if not match:
|
|
1950
|
+
typer.echo(f"Session '{detail}' not found.")
|
|
1951
|
+
raise typer.Exit(1)
|
|
1952
|
+
if json_output:
|
|
1953
|
+
typer.echo(json_mod.dumps(match, indent=2, default=str))
|
|
1954
|
+
else:
|
|
1955
|
+
typer.echo(render_session_detail(match), nl=False)
|
|
1956
|
+
else:
|
|
1957
|
+
data = db.get_sessions_with_quality(limit=last, source=source)
|
|
1958
|
+
if json_output:
|
|
1959
|
+
typer.echo(json_mod.dumps(data, indent=2, default=str))
|
|
1960
|
+
else:
|
|
1961
|
+
typer.echo(render_sessions_table(data), nl=False)
|
|
1962
|
+
|
|
1963
|
+
from reprompt.core.suggestions import get_suggestion
|
|
1964
|
+
|
|
1965
|
+
hint = get_suggestion("sessions")
|
|
1966
|
+
if hint:
|
|
1967
|
+
console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
|
|
1968
|
+
|
|
1969
|
+
if copy:
|
|
1970
|
+
if detail:
|
|
1971
|
+
copy_text = json_mod.dumps(match, indent=2, default=str) # type: ignore[possibly-undefined]
|
|
1972
|
+
else:
|
|
1973
|
+
copy_text = json_mod.dumps(data, indent=2, default=str) # type: ignore[possibly-undefined]
|
|
1974
|
+
_copy_to_clip(copy_text, quiet=json_output)
|
|
1975
|
+
|
|
1976
|
+
|
|
1919
1977
|
@app.command(rich_help_panel="Analyze")
|
|
1920
1978
|
def agent(
|
|
1921
1979
|
last: int = typer.Option(5, "--last", help="Analyze N most recent sessions"),
|
|
@@ -185,6 +185,105 @@ def run_scan(
|
|
|
185
185
|
except Exception:
|
|
186
186
|
logger.debug("Session metadata extraction failed for %s", file_path, exc_info=True)
|
|
187
187
|
|
|
188
|
+
# Compute session quality scores
|
|
189
|
+
from datetime import datetime
|
|
190
|
+
|
|
191
|
+
from reprompt.core.agent import analyze_session
|
|
192
|
+
from reprompt.core.conversation import Conversation
|
|
193
|
+
from reprompt.core.distill import distill_conversation
|
|
194
|
+
from reprompt.core.session_quality import score_session
|
|
195
|
+
|
|
196
|
+
quality_failures = 0
|
|
197
|
+
for file_path, adapter_name in scanned_files:
|
|
198
|
+
try:
|
|
199
|
+
matched = next((a for a in adapters if a.name == adapter_name), None)
|
|
200
|
+
if not matched:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# parse_conversation returns list[ConversationTurn], wrap into Conversation
|
|
204
|
+
turns = matched.parse_conversation(Path(file_path))
|
|
205
|
+
if not turns:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
session_id = Path(file_path).stem
|
|
209
|
+
start_time = None
|
|
210
|
+
end_time = None
|
|
211
|
+
duration = None
|
|
212
|
+
timestamps = [t.timestamp for t in turns if t.timestamp]
|
|
213
|
+
if len(timestamps) >= 2:
|
|
214
|
+
start_time = timestamps[0]
|
|
215
|
+
end_time = timestamps[-1]
|
|
216
|
+
try:
|
|
217
|
+
start_dt = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
|
|
218
|
+
end_dt = datetime.fromisoformat(end_time.replace("Z", "+00:00"))
|
|
219
|
+
duration = int((end_dt - start_dt).total_seconds())
|
|
220
|
+
except (ValueError, TypeError):
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
project = None
|
|
224
|
+
if hasattr(matched, "_project_from_path"):
|
|
225
|
+
project = matched._project_from_path(file_path)
|
|
226
|
+
|
|
227
|
+
conversation = Conversation(
|
|
228
|
+
session_id=session_id,
|
|
229
|
+
source=adapter_name,
|
|
230
|
+
project=project,
|
|
231
|
+
turns=turns,
|
|
232
|
+
start_time=start_time,
|
|
233
|
+
end_time=end_time,
|
|
234
|
+
duration_seconds=duration,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Agent analysis (efficiency, error loops)
|
|
238
|
+
agent_report = None
|
|
239
|
+
try:
|
|
240
|
+
agent_report = analyze_session(conversation)
|
|
241
|
+
except Exception:
|
|
242
|
+
logger.warning("Agent analysis failed for %s", file_path, exc_info=True)
|
|
243
|
+
|
|
244
|
+
# Distill analysis (focus/retention)
|
|
245
|
+
distill_result = None
|
|
246
|
+
try:
|
|
247
|
+
distill_result = distill_conversation(conversation)
|
|
248
|
+
except Exception:
|
|
249
|
+
logger.warning("Distill analysis failed for %s", file_path, exc_info=True)
|
|
250
|
+
|
|
251
|
+
# Avg prompt score from stored features
|
|
252
|
+
avg_prompt_score = None
|
|
253
|
+
scores = db.get_prompt_scores_for_session(session_id)
|
|
254
|
+
if scores:
|
|
255
|
+
avg_prompt_score = sum(scores) / len(scores)
|
|
256
|
+
|
|
257
|
+
# Effectiveness score from session_meta
|
|
258
|
+
effectiveness = db.get_effectiveness_for_session(session_id)
|
|
259
|
+
|
|
260
|
+
quality = score_session(
|
|
261
|
+
conversation,
|
|
262
|
+
agent_report=agent_report,
|
|
263
|
+
distill_result=distill_result,
|
|
264
|
+
effectiveness_score=effectiveness,
|
|
265
|
+
avg_prompt_score=avg_prompt_score,
|
|
266
|
+
)
|
|
267
|
+
db.upsert_session_quality(
|
|
268
|
+
session_id=quality.session_id,
|
|
269
|
+
quality_score=quality.quality_score,
|
|
270
|
+
prompt_quality_score=quality.prompt_quality,
|
|
271
|
+
efficiency_score=quality.efficiency,
|
|
272
|
+
focus_score=quality.focus,
|
|
273
|
+
outcome_score=quality.outcome,
|
|
274
|
+
has_abandonment=quality.frustration.abandonment,
|
|
275
|
+
has_escalation=quality.frustration.escalation,
|
|
276
|
+
stall_turns=quality.frustration.stall_turns,
|
|
277
|
+
session_type=quality.session_type,
|
|
278
|
+
quality_insight=quality.insight,
|
|
279
|
+
)
|
|
280
|
+
except Exception:
|
|
281
|
+
quality_failures += 1
|
|
282
|
+
logger.warning("Session quality scoring failed for %s", file_path, exc_info=True)
|
|
283
|
+
|
|
284
|
+
if quality_failures:
|
|
285
|
+
logger.warning("Quality scoring failed for %d session(s)", quality_failures)
|
|
286
|
+
|
|
188
287
|
# Mark sessions processed only after successful dedup+store
|
|
189
288
|
for file_path, adapter_name in scanned_files:
|
|
190
289
|
db.mark_session_processed(file_path, source=adapter_name)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Session-level quality metrics — composite scoring + frustration detection.
|
|
2
|
+
|
|
3
|
+
Combines prompt quality, agent efficiency, distill focus, and outcome
|
|
4
|
+
into a single 0-100 session score. All analysis is rule-based (zero LLM, <1ms).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from reprompt.core.agent import AgentReport
|
|
12
|
+
from reprompt.core.conversation import Conversation, ConversationTurn, DistillResult
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Component weights (must sum to 1.0)
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
DEFAULT_WEIGHTS: dict[str, float] = {
|
|
19
|
+
"prompt_quality": 0.30,
|
|
20
|
+
"efficiency": 0.30,
|
|
21
|
+
"focus": 0.20,
|
|
22
|
+
"outcome": 0.20,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Dataclasses
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class FrustrationSignals:
|
|
33
|
+
"""Frustration indicators detected from conversation turns."""
|
|
34
|
+
|
|
35
|
+
abandonment: bool = False # Last 3+ assistant turns all have errors
|
|
36
|
+
escalation: bool = False # Error rate in second half > first half * 1.5
|
|
37
|
+
stall_turns: int = 0 # Assistant turns with 0 tool calls and <50 chars
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SessionQuality:
|
|
42
|
+
"""Composite session quality report."""
|
|
43
|
+
|
|
44
|
+
session_id: str
|
|
45
|
+
quality_score: float # 0-100 composite
|
|
46
|
+
prompt_quality: float | None = None # 0-100
|
|
47
|
+
efficiency: float | None = None # 0-100
|
|
48
|
+
focus: float | None = None # 0-100
|
|
49
|
+
outcome: float | None = None # 0-100
|
|
50
|
+
frustration: FrustrationSignals = field(default_factory=FrustrationSignals)
|
|
51
|
+
session_type: str | None = None
|
|
52
|
+
insight: str = ""
|
|
53
|
+
components_available: int = 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Frustration detection
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _detect_frustration(turns: list[ConversationTurn]) -> FrustrationSignals:
|
|
62
|
+
"""Detect frustration signals from conversation turns."""
|
|
63
|
+
asst_turns = [t for t in turns if t.role == "assistant"]
|
|
64
|
+
|
|
65
|
+
if not asst_turns:
|
|
66
|
+
return FrustrationSignals()
|
|
67
|
+
|
|
68
|
+
# Abandonment: last 3+ assistant turns all have errors
|
|
69
|
+
abandonment = False
|
|
70
|
+
if len(asst_turns) >= 3:
|
|
71
|
+
last_three = asst_turns[-3:]
|
|
72
|
+
abandonment = all(t.has_error for t in last_three)
|
|
73
|
+
|
|
74
|
+
# Escalation: error rate in second half > first half * 1.5
|
|
75
|
+
escalation = False
|
|
76
|
+
if len(asst_turns) >= 4:
|
|
77
|
+
mid = len(asst_turns) // 2
|
|
78
|
+
first_half = asst_turns[:mid]
|
|
79
|
+
second_half = asst_turns[mid:]
|
|
80
|
+
first_rate = sum(1 for t in first_half if t.has_error) / len(first_half)
|
|
81
|
+
second_rate = sum(1 for t in second_half if t.has_error) / len(second_half)
|
|
82
|
+
escalation = second_rate > first_rate * 1.5 and second_rate > 0.2
|
|
83
|
+
|
|
84
|
+
# Stall turns: assistant turns with no tool calls and short text
|
|
85
|
+
stall_turns = sum(
|
|
86
|
+
1 for t in asst_turns if t.tool_calls == 0 and len(t.text.strip()) < 50
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return FrustrationSignals(
|
|
90
|
+
abandonment=abandonment,
|
|
91
|
+
escalation=escalation,
|
|
92
|
+
stall_turns=stall_turns,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Insight generation
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _generate_insight(quality: SessionQuality) -> str:
|
|
102
|
+
"""Generate a one-line insight from quality metrics (priority order)."""
|
|
103
|
+
f = quality.frustration
|
|
104
|
+
|
|
105
|
+
if f.abandonment:
|
|
106
|
+
return "Ended with unresolved errors"
|
|
107
|
+
|
|
108
|
+
if f.escalation:
|
|
109
|
+
return "Errors escalated through session"
|
|
110
|
+
|
|
111
|
+
if f.stall_turns >= 5:
|
|
112
|
+
return f"{f.stall_turns} stall turns detected"
|
|
113
|
+
|
|
114
|
+
if quality.efficiency is not None and quality.efficiency < 50:
|
|
115
|
+
return "Low efficiency (error loops)"
|
|
116
|
+
|
|
117
|
+
score = quality.quality_score
|
|
118
|
+
if score >= 80:
|
|
119
|
+
return "Focused session"
|
|
120
|
+
if score >= 60:
|
|
121
|
+
return "Solid session"
|
|
122
|
+
if score >= 40:
|
|
123
|
+
return "Room for improvement"
|
|
124
|
+
return "Rough session"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
# Main scoring function
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def score_session(
|
|
133
|
+
conversation: Conversation,
|
|
134
|
+
*,
|
|
135
|
+
agent_report: AgentReport | None = None,
|
|
136
|
+
distill_result: DistillResult | None = None,
|
|
137
|
+
effectiveness_score: float | None = None,
|
|
138
|
+
avg_prompt_score: float | None = None,
|
|
139
|
+
) -> SessionQuality:
|
|
140
|
+
"""Compute composite session quality score (0-100).
|
|
141
|
+
|
|
142
|
+
Components (weighted average of available inputs):
|
|
143
|
+
- prompt_quality (30%): avg overall_score from prompt features
|
|
144
|
+
- efficiency (30%): productive_ratio from agent analysis
|
|
145
|
+
- focus (20%): retention_ratio from distill
|
|
146
|
+
- outcome (20%): effectiveness_score
|
|
147
|
+
|
|
148
|
+
When a component is unavailable, its weight redistributes proportionally.
|
|
149
|
+
"""
|
|
150
|
+
components: dict[str, float] = {}
|
|
151
|
+
|
|
152
|
+
# Prompt quality: already 0-100
|
|
153
|
+
if avg_prompt_score is not None:
|
|
154
|
+
components["prompt_quality"] = max(0.0, min(100.0, avg_prompt_score))
|
|
155
|
+
|
|
156
|
+
# Efficiency: productive_ratio is 0-1
|
|
157
|
+
if agent_report is not None:
|
|
158
|
+
ratio = agent_report.efficiency.productive_ratio
|
|
159
|
+
components["efficiency"] = max(0.0, min(100.0, ratio * 100))
|
|
160
|
+
|
|
161
|
+
# Focus: retention_ratio is 0-1
|
|
162
|
+
if distill_result is not None:
|
|
163
|
+
ratio = distill_result.stats.retention_ratio
|
|
164
|
+
components["focus"] = max(0.0, min(100.0, ratio * 100))
|
|
165
|
+
|
|
166
|
+
# Outcome: effectiveness_score is 0-1
|
|
167
|
+
if effectiveness_score is not None:
|
|
168
|
+
components["outcome"] = max(0.0, min(100.0, effectiveness_score * 100))
|
|
169
|
+
|
|
170
|
+
# Compute weighted average with weight redistribution
|
|
171
|
+
if components:
|
|
172
|
+
available_weights = {k: DEFAULT_WEIGHTS[k] for k in components}
|
|
173
|
+
weight_sum = sum(available_weights.values())
|
|
174
|
+
normalized = {k: w / weight_sum for k, w in available_weights.items()}
|
|
175
|
+
quality_score = sum(components[k] * normalized[k] for k in components)
|
|
176
|
+
else:
|
|
177
|
+
quality_score = 0.0
|
|
178
|
+
|
|
179
|
+
quality_score = round(max(0.0, min(100.0, quality_score)), 1)
|
|
180
|
+
|
|
181
|
+
# Frustration detection
|
|
182
|
+
frustration = _detect_frustration(conversation.turns)
|
|
183
|
+
|
|
184
|
+
# Session type
|
|
185
|
+
session_type_str: str | None = None
|
|
186
|
+
if agent_report is not None and agent_report.efficiency.session_type is not None:
|
|
187
|
+
session_type_str = agent_report.efficiency.session_type
|
|
188
|
+
|
|
189
|
+
quality = SessionQuality(
|
|
190
|
+
session_id=conversation.session_id,
|
|
191
|
+
quality_score=quality_score,
|
|
192
|
+
prompt_quality=components.get("prompt_quality"),
|
|
193
|
+
efficiency=components.get("efficiency"),
|
|
194
|
+
focus=components.get("focus"),
|
|
195
|
+
outcome=components.get("outcome"),
|
|
196
|
+
frustration=frustration,
|
|
197
|
+
session_type=session_type_str,
|
|
198
|
+
components_available=len(components),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
quality.insight = _generate_insight(quality)
|
|
202
|
+
|
|
203
|
+
return quality
|
|
@@ -15,6 +15,9 @@ SUGGESTIONS: dict[str, str] = {
|
|
|
15
15
|
"agent": (
|
|
16
16
|
"reprompt agent --loops-only (error loops) · reprompt privacy --deep (sensitive content)"
|
|
17
17
|
),
|
|
18
|
+
"sessions": (
|
|
19
|
+
"reprompt sessions --detail <id> (deep-dive) · reprompt agent (error loop analysis)"
|
|
20
|
+
),
|
|
18
21
|
"template": "reprompt insights (see which patterns work best)",
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Rich terminal rendering for session quality metrics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _score_style(score: float | None) -> str:
|
|
14
|
+
"""Return Rich style string for a quality score."""
|
|
15
|
+
if score is None:
|
|
16
|
+
return "dim"
|
|
17
|
+
if score >= 80:
|
|
18
|
+
return "green"
|
|
19
|
+
if score >= 60:
|
|
20
|
+
return "yellow"
|
|
21
|
+
if score >= 40:
|
|
22
|
+
return "dim"
|
|
23
|
+
return "red"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fmt_score(score: float | None) -> str:
|
|
27
|
+
if score is None:
|
|
28
|
+
return "—"
|
|
29
|
+
return f"{score:.0f}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fmt_duration(seconds: int | None) -> str:
|
|
33
|
+
if seconds is None:
|
|
34
|
+
return "—"
|
|
35
|
+
if seconds < 60:
|
|
36
|
+
return f"{seconds}s"
|
|
37
|
+
return f"{seconds // 60}min"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _bar(value: float, width: int = 10) -> str:
|
|
41
|
+
"""Render a 0-100 value as a bar chart."""
|
|
42
|
+
filled = max(0, min(width, round(value / 100 * width)))
|
|
43
|
+
return "\u2588" * filled + "\u2591" * (width - filled)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def render_sessions_table(sessions: list[dict[str, Any]]) -> str:
|
|
47
|
+
"""Render session quality overview as formatted terminal output."""
|
|
48
|
+
console = Console(record=True, width=100, file=StringIO())
|
|
49
|
+
|
|
50
|
+
if not sessions:
|
|
51
|
+
console.print("[dim]No sessions with quality scores found.[/dim]")
|
|
52
|
+
console.print(
|
|
53
|
+
"Run [bold cyan]reprompt scan[/bold cyan] to import and score sessions."
|
|
54
|
+
)
|
|
55
|
+
return console.export_text()
|
|
56
|
+
|
|
57
|
+
# Compute avg quality
|
|
58
|
+
scored = [s for s in sessions if s.get("quality_score") is not None]
|
|
59
|
+
avg_q = sum(s["quality_score"] for s in scored) / len(scored) if scored else 0
|
|
60
|
+
|
|
61
|
+
# Header
|
|
62
|
+
header = (
|
|
63
|
+
f"Sessions: {len(sessions)} | "
|
|
64
|
+
f"Scored: {len(scored)} | "
|
|
65
|
+
f"Avg Quality: {avg_q:.0f}/100"
|
|
66
|
+
)
|
|
67
|
+
console.print(Panel(header, title="Session Quality", border_style="cyan"))
|
|
68
|
+
|
|
69
|
+
# Table
|
|
70
|
+
table = Table(show_header=True, header_style="bold", padding=(0, 1))
|
|
71
|
+
table.add_column("Session", max_width=20)
|
|
72
|
+
table.add_column("Score", justify="right", width=5)
|
|
73
|
+
table.add_column("Type", width=6)
|
|
74
|
+
table.add_column("Turns", justify="right", width=5)
|
|
75
|
+
table.add_column("Errors", justify="right", width=6)
|
|
76
|
+
table.add_column("Duration", justify="right", width=8)
|
|
77
|
+
table.add_column("Insight", max_width=30)
|
|
78
|
+
|
|
79
|
+
for s in sessions:
|
|
80
|
+
score = s.get("quality_score")
|
|
81
|
+
style = _score_style(score)
|
|
82
|
+
sid = (s.get("session_id") or "")[:20]
|
|
83
|
+
stype = (s.get("session_type") or "")[:6]
|
|
84
|
+
turns = str(s.get("prompt_count") or "—")
|
|
85
|
+
errors = str(s.get("error_count") or "0")
|
|
86
|
+
duration = _fmt_duration(s.get("duration_seconds"))
|
|
87
|
+
insight = s.get("quality_insight") or ""
|
|
88
|
+
|
|
89
|
+
table.add_row(
|
|
90
|
+
sid,
|
|
91
|
+
f"[{style}]{_fmt_score(score)}[/{style}]",
|
|
92
|
+
stype,
|
|
93
|
+
turns,
|
|
94
|
+
errors,
|
|
95
|
+
duration,
|
|
96
|
+
insight,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
console.print(table)
|
|
100
|
+
console.print()
|
|
101
|
+
return console.export_text()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def render_session_detail(session: dict[str, Any]) -> str:
|
|
105
|
+
"""Render a single session's quality breakdown."""
|
|
106
|
+
console = Console(record=True, width=100, file=StringIO())
|
|
107
|
+
|
|
108
|
+
sid = session.get("session_id", "unknown")
|
|
109
|
+
score = session.get("quality_score")
|
|
110
|
+
style = _score_style(score)
|
|
111
|
+
|
|
112
|
+
console.print(
|
|
113
|
+
Panel(
|
|
114
|
+
f"Session: {sid} | Score: [{style}]{_fmt_score(score)}/100[/{style}]",
|
|
115
|
+
title="Session Detail",
|
|
116
|
+
border_style="cyan",
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Component scores
|
|
121
|
+
console.print()
|
|
122
|
+
console.print(" [bold]Quality Components[/bold]")
|
|
123
|
+
console.print(" \u2500" * 50)
|
|
124
|
+
|
|
125
|
+
components = [
|
|
126
|
+
("Prompt Quality", session.get("prompt_quality_score"), "30%"),
|
|
127
|
+
("Efficiency", session.get("efficiency_score"), "30%"),
|
|
128
|
+
("Focus", session.get("focus_score"), "20%"),
|
|
129
|
+
("Outcome", session.get("outcome_score"), "20%"),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
for name, val, weight in components:
|
|
133
|
+
if val is not None:
|
|
134
|
+
bar = _bar(val)
|
|
135
|
+
cs = _score_style(val)
|
|
136
|
+
console.print(f" {name:<16} {bar} [{cs}]{val:.0f}[/{cs}] (weight: {weight})")
|
|
137
|
+
else:
|
|
138
|
+
console.print(f" {name:<16} [dim]not available[/dim] (weight: {weight})")
|
|
139
|
+
|
|
140
|
+
# Frustration signals
|
|
141
|
+
console.print()
|
|
142
|
+
console.print(" [bold]Frustration Signals[/bold]")
|
|
143
|
+
console.print(" \u2500" * 50)
|
|
144
|
+
|
|
145
|
+
signals = []
|
|
146
|
+
if session.get("has_abandonment"):
|
|
147
|
+
signals.append("[red]Abandonment[/red] — session ended with unresolved errors")
|
|
148
|
+
if session.get("has_escalation"):
|
|
149
|
+
signals.append("[red]Escalation[/red] — errors increased through session")
|
|
150
|
+
stalls = session.get("stall_turns", 0)
|
|
151
|
+
if stalls > 0:
|
|
152
|
+
signals.append(f"[yellow]Stall turns[/yellow] — {stalls} turns with no tool use")
|
|
153
|
+
|
|
154
|
+
if signals:
|
|
155
|
+
for sig in signals:
|
|
156
|
+
console.print(f" \u26a0 {sig}")
|
|
157
|
+
else:
|
|
158
|
+
console.print(" [green]None detected[/green]")
|
|
159
|
+
|
|
160
|
+
# Session info
|
|
161
|
+
console.print()
|
|
162
|
+
console.print(" [bold]Session Info[/bold]")
|
|
163
|
+
console.print(" \u2500" * 50)
|
|
164
|
+
console.print(f" Source: {session.get('source', '—')}")
|
|
165
|
+
console.print(f" Type: {session.get('session_type') or '—'}")
|
|
166
|
+
console.print(f" Duration: {_fmt_duration(session.get('duration_seconds'))}")
|
|
167
|
+
console.print(f" Prompts: {session.get('prompt_count', '—')}")
|
|
168
|
+
console.print(f" Errors: {session.get('error_count', 0)}")
|
|
169
|
+
console.print(f" Insight: {session.get('quality_insight') or '—'}")
|
|
170
|
+
|
|
171
|
+
console.print()
|
|
172
|
+
return console.export_text()
|