reprompt-cli 1.8.0__tar.gz → 1.9.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.8.0 → reprompt_cli-1.9.0}/PKG-INFO +1 -1
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/pyproject.toml +1 -1
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/__init__.py +1 -1
- reprompt_cli-1.9.0/src/reprompt/bridge/handler.py +234 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/cli.py +51 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/insights.py +26 -0
- reprompt_cli-1.9.0/src/reprompt/core/repetition.py +128 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/session_quality.py +1 -3
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/suggestions.py +3 -0
- reprompt_cli-1.9.0/src/reprompt/output/repetition_terminal.py +61 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/sessions_terminal.py +2 -8
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/storage/db.py +17 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_bridge_e2e.py +19 -19
- reprompt_cli-1.9.0/tests/test_bridge_handler.py +354 -0
- reprompt_cli-1.9.0/tests/test_repetition.py +124 -0
- reprompt_cli-1.9.0/tests/test_repetition_cli.py +102 -0
- reprompt_cli-1.9.0/tests/test_repetition_output.py +76 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_suggestions.py +9 -2
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/uv.lock +1 -1
- reprompt_cli-1.8.0/src/reprompt/bridge/handler.py +0 -100
- reprompt_cli-1.8.0/tests/test_bridge_handler.py +0 -118
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.editorconfig +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.github/dependabot.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.github/workflows/ci.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.github/workflows/publish.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.gitignore +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.pre-commit-config.yaml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.pre-commit-hooks.yaml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.testmondata-shm +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/.testmondata-wal +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/CHANGELOG.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/CODE_OF_CONDUCT.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/CONTRIBUTING.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/LICENSE +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/README.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/SECURITY.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/action.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/demo.gif +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/brand-icon-128.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/brand-icon-16.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/brand-icon-256.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/brand-icon-32.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/brand-icon-48.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/brand-icon-512.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/brand-icon-96.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/brand-icon.svg +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-128.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-16.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-256.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-32.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-48.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-512.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-96.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-favicon.svg +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-icon-128.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-icon-16.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-icon-256.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-icon-32.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-icon-48.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-icon-512.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-icon-96.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/cli-icon.svg +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/favicon-128.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/favicon-16.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/favicon-256.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/favicon-32.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/favicon-48.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/favicon-512.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/favicon-96.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/favicon.svg +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/icons/generate.sh +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/scripts/generate_demo_data.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/aider.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/base.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/chatgpt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/claude_chat.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/claude_code.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/cline.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/codex.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/cursor.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/filters.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/gemini.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/adapters/openclaw.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/bridge/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/bridge/host.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/bridge/manifest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/bridge/protocol.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/commands/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/commands/telemetry.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/commands/wrapped.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/config.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/agent.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/analyzer.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/compress.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/conversation.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/cost.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/dashboard.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/dedup.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/digest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/distill.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/effectiveness.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/extractors.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/extractors_zh.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/lang_detect.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/library.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/lint.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/merge_view.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/models.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/persona.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/pipeline.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/privacy.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/privacy_scan.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/prompt_dna.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/recommend.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/scorer.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/segmenter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/session_meta.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/session_type.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/style.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/templates.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/timeutil.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/trends.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/core/wrapped.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/demo.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/embeddings/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/embeddings/base.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/embeddings/local_embed.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/embeddings/ollama.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/embeddings/openai_embed.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/embeddings/tfidf.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/mcp.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/mcp_main.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/agent_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/chartjs.min.js +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/compress_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/dashboard_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/distill_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/export.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/html_report.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/json_out.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/markdown.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/wrapped_html.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/output/wrapped_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/py.typed +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/sharing/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/sharing/client.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/sharing/clipboard.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/storage/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/telemetry/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/telemetry/collector.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/telemetry/consent.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/telemetry/events.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/telemetry/prompt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/telemetry/queue.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/src/reprompt/telemetry/sender.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/conftest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/fixtures/aider_chat_history.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/fixtures/chatgpt_conversations.json +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/fixtures/claude_chat_export.json +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/fixtures/claude_session.jsonl +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/fixtures/export/default_export.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/fixtures/export/full_export.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/fixtures/gemini_session.json +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/fixtures/openclaw_session.jsonl +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_adapter_aider.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_adapter_chatgpt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_adapter_claude.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_adapter_claude_chat.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_adapter_cline.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_adapter_gemini.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_adapter_openclaw.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_agent.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_agent_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_analyzer.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_bridge_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_bridge_integration.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_bridge_manifest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_bridge_protocol.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_cli_deprecations.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_cli_library_effectiveness.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_clipboard.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_codex_adapter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_compare_best_worst.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_compress.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_compress_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_compress_dna.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_compress_html.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_compress_insights.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_config.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_conversation.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_copy_flag.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_cost.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_coverage_boost.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_cursor_adapter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_dashboard.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_db.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_db_digest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_db_effectiveness.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_db_session_quality.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_db_trends.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_dedup.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_demo.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_deprecated_commands.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_digest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_digest_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_distill.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_distill_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_distill_weights.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_effectiveness.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_embeddings_local.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_embeddings_ollama.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_embeddings_openai.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_empty_state.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_export.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_export_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_export_snapshot.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_extractors.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_extractors_routing.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_extractors_zh.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_extractors_zh_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_html_report.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_import_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_import_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_insights.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_insights_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_insights_expanded.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_install_hook.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_lang_detect.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_library.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_lint.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_lint_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_markdown.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_mcp.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_merge_view.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_models.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_output.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_parse_conversation_base.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_parse_conversation_chatgpt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_parse_conversation_claude.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_persona.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_pipeline.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_privacy.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_privacy_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_privacy_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_privacy_output.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_privacy_scan.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_prompt_dna.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_public_api.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_recommend.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_schema_version.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_score_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_scorer.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_segmenter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_session_quality.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_session_type.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_sessions_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_sessions_output.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_share_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_sharing_client.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_source_filter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_style.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_style_trends.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_telemetry_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_telemetry_collector.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_telemetry_consent.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_telemetry_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_telemetry_events.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_telemetry_prompt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_telemetry_queue.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_telemetry_sender.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_template_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_templates.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_timeutil.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_trends.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_trends_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_use_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_wrapped.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_wrapped_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_wrapped_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_wrapped_html.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.0}/tests/test_wrapped_output.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.9.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.9.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
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Message handler for Native Messaging bridge.
|
|
2
|
+
|
|
3
|
+
Processes incoming messages from the browser extension:
|
|
4
|
+
- ping -> pong (health check)
|
|
5
|
+
- sync_prompts -> store in DB, return counts + lightweight insights
|
|
6
|
+
- get_status -> return DB stats
|
|
7
|
+
- get_insights -> return full analysis (repetition, patterns, top insight)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from reprompt import __version__
|
|
17
|
+
from reprompt.adapters.filters import should_keep_prompt
|
|
18
|
+
from reprompt.storage.db import PromptDB
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def handle_message(message: dict[str, Any], db: PromptDB) -> dict[str, Any]:
|
|
24
|
+
"""Process a single message and return a response dict."""
|
|
25
|
+
msg_type = message.get("type", "")
|
|
26
|
+
|
|
27
|
+
if msg_type == "ping":
|
|
28
|
+
return {"type": "pong", "version": __version__}
|
|
29
|
+
|
|
30
|
+
if msg_type == "sync_prompts":
|
|
31
|
+
return _handle_sync(message, db)
|
|
32
|
+
|
|
33
|
+
if msg_type == "get_status":
|
|
34
|
+
return _handle_status(db)
|
|
35
|
+
|
|
36
|
+
if msg_type == "get_insights":
|
|
37
|
+
return _handle_insights(message, db)
|
|
38
|
+
|
|
39
|
+
return {"type": "error", "message": f"Unknown message type: {msg_type}"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _handle_sync(message: dict[str, Any], db: PromptDB) -> dict[str, Any]:
|
|
43
|
+
"""Store synced prompts in DB, skipping noise and duplicates."""
|
|
44
|
+
prompts = message.get("prompts", [])
|
|
45
|
+
received = len(prompts)
|
|
46
|
+
new_stored = 0
|
|
47
|
+
duplicates = 0
|
|
48
|
+
|
|
49
|
+
for p in prompts:
|
|
50
|
+
text = p.get("text", "").strip()
|
|
51
|
+
if not should_keep_prompt(text):
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
source = p.get("source", "extension")
|
|
55
|
+
session_id = p.get("conversation_id", "")
|
|
56
|
+
project = p.get("conversation_title", "")
|
|
57
|
+
timestamp = p.get("timestamp", "")
|
|
58
|
+
|
|
59
|
+
inserted = db.insert_prompt(
|
|
60
|
+
text,
|
|
61
|
+
source=source,
|
|
62
|
+
project=project,
|
|
63
|
+
session_id=session_id,
|
|
64
|
+
timestamp=timestamp,
|
|
65
|
+
)
|
|
66
|
+
if inserted:
|
|
67
|
+
new_stored += 1
|
|
68
|
+
else:
|
|
69
|
+
duplicates += 1
|
|
70
|
+
|
|
71
|
+
# Record last sync time
|
|
72
|
+
_update_last_sync(db)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"type": "sync_result",
|
|
76
|
+
"received": received,
|
|
77
|
+
"new_stored": new_stored,
|
|
78
|
+
"duplicates": duplicates,
|
|
79
|
+
"insights": _compute_quick_insights(db),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _handle_status(db: PromptDB) -> dict[str, Any]:
|
|
84
|
+
"""Return current database stats."""
|
|
85
|
+
stats = db.get_stats()
|
|
86
|
+
return {
|
|
87
|
+
"type": "status",
|
|
88
|
+
"total_prompts": stats.get("total_prompts", 0),
|
|
89
|
+
"last_sync": _get_last_sync(db),
|
|
90
|
+
"version": __version__,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _handle_insights(message: dict[str, Any], db: PromptDB) -> dict[str, Any]:
|
|
95
|
+
"""Return full analysis: repetition, effectiveness patterns, insights.
|
|
96
|
+
|
|
97
|
+
Heavier computation than sync — call on-demand, not every sync.
|
|
98
|
+
"""
|
|
99
|
+
source = message.get("source")
|
|
100
|
+
result: dict[str, Any] = {"type": "insights_result"}
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
from reprompt.core.insights import (
|
|
104
|
+
compute_insights,
|
|
105
|
+
get_cross_session_repetition_insight,
|
|
106
|
+
get_effectiveness_insight,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
features = db.get_all_features(source=source)
|
|
110
|
+
if features:
|
|
111
|
+
full = compute_insights(features)
|
|
112
|
+
result["avg_score"] = full.get("avg_score", 0.0)
|
|
113
|
+
result["prompt_count"] = full.get("prompt_count", 0)
|
|
114
|
+
result["score_distribution"] = full.get("score_distribution", {})
|
|
115
|
+
result["insights"] = [
|
|
116
|
+
{"category": i["category"], "action": i["action"], "impact": i["impact"]}
|
|
117
|
+
for i in full.get("insights", [])
|
|
118
|
+
]
|
|
119
|
+
else:
|
|
120
|
+
result["avg_score"] = 0.0
|
|
121
|
+
result["prompt_count"] = 0
|
|
122
|
+
result["score_distribution"] = {}
|
|
123
|
+
result["insights"] = []
|
|
124
|
+
|
|
125
|
+
# Repetition (may be None if insufficient data)
|
|
126
|
+
rep = get_cross_session_repetition_insight(db, source=source)
|
|
127
|
+
if rep:
|
|
128
|
+
result["repetition"] = {
|
|
129
|
+
"rate": rep["repetition_rate"],
|
|
130
|
+
"top_topics": rep["top_topics"],
|
|
131
|
+
"total_recurring": rep["total_recurring_topics"],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Effectiveness patterns (may be None)
|
|
135
|
+
eff = get_effectiveness_insight(db, source=source)
|
|
136
|
+
if eff:
|
|
137
|
+
result["effectiveness"] = {
|
|
138
|
+
"top_patterns": eff["top_patterns"],
|
|
139
|
+
"total_patterns": eff["total_patterns"],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
except Exception:
|
|
143
|
+
logger.warning("Failed to compute full insights for extension", exc_info=True)
|
|
144
|
+
result["error"] = "Failed to compute insights"
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _compute_quick_insights(db: PromptDB) -> dict[str, Any]:
|
|
150
|
+
"""Lightweight stats for extension display. Pure SQL, no heavy computation."""
|
|
151
|
+
stats = db.get_stats()
|
|
152
|
+
total = stats.get("total_prompts", 0)
|
|
153
|
+
|
|
154
|
+
if total == 0:
|
|
155
|
+
return {
|
|
156
|
+
"avg_score": 0.0,
|
|
157
|
+
"total_prompts": 0,
|
|
158
|
+
"score_trend": "stable",
|
|
159
|
+
"top_insight": None,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
scores = db.get_recent_scores(limit=50)
|
|
163
|
+
|
|
164
|
+
if not scores:
|
|
165
|
+
return {
|
|
166
|
+
"avg_score": 0.0,
|
|
167
|
+
"total_prompts": total,
|
|
168
|
+
"score_trend": "stable",
|
|
169
|
+
"top_insight": None,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
avg_score = round(sum(scores) / len(scores), 1)
|
|
173
|
+
|
|
174
|
+
# Trend: compare first half (recent) vs second half (older)
|
|
175
|
+
mid = len(scores) // 2
|
|
176
|
+
if mid >= 5:
|
|
177
|
+
recent_avg = sum(scores[:mid]) / mid
|
|
178
|
+
older_avg = sum(scores[mid:]) / (len(scores) - mid)
|
|
179
|
+
diff = recent_avg - older_avg
|
|
180
|
+
if diff > 3:
|
|
181
|
+
trend = "improving"
|
|
182
|
+
elif diff < -3:
|
|
183
|
+
trend = "declining"
|
|
184
|
+
else:
|
|
185
|
+
trend = "stable"
|
|
186
|
+
else:
|
|
187
|
+
trend = "stable"
|
|
188
|
+
|
|
189
|
+
# Top insight: get highest-impact actionable tip
|
|
190
|
+
top_insight = _get_top_insight(db)
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
"avg_score": avg_score,
|
|
194
|
+
"total_prompts": total,
|
|
195
|
+
"score_trend": trend,
|
|
196
|
+
"top_insight": top_insight,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _get_top_insight(db: PromptDB) -> str | None:
|
|
201
|
+
"""Return the single most impactful insight as a string, or None."""
|
|
202
|
+
try:
|
|
203
|
+
from reprompt.core.insights import compute_insights
|
|
204
|
+
|
|
205
|
+
features = db.get_all_features()
|
|
206
|
+
if len(features) < 5:
|
|
207
|
+
return None
|
|
208
|
+
result = compute_insights(features)
|
|
209
|
+
insights = result.get("insights", [])
|
|
210
|
+
# Prioritize high-impact insights
|
|
211
|
+
for impact in ("high", "medium", "low"):
|
|
212
|
+
for i in insights:
|
|
213
|
+
if i.get("impact") == impact:
|
|
214
|
+
return i["action"]
|
|
215
|
+
except Exception:
|
|
216
|
+
logger.debug("Failed to compute top insight", exc_info=True)
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _update_last_sync(db: PromptDB) -> None:
|
|
221
|
+
"""Store last sync timestamp in the DB settings table."""
|
|
222
|
+
now_ts = str(int(datetime.now(tz=timezone.utc).timestamp()))
|
|
223
|
+
db.set_setting("last_extension_sync", now_ts)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _get_last_sync(db: PromptDB) -> str:
|
|
227
|
+
"""Get last sync timestamp. Returns empty string if never synced."""
|
|
228
|
+
val = db.get_setting("last_extension_sync")
|
|
229
|
+
if val:
|
|
230
|
+
try:
|
|
231
|
+
return datetime.fromtimestamp(int(val), tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
232
|
+
except (ValueError, OSError):
|
|
233
|
+
return ""
|
|
234
|
+
return ""
|
|
@@ -1474,6 +1474,7 @@ def insights(
|
|
|
1474
1474
|
from reprompt.config import Settings
|
|
1475
1475
|
from reprompt.core.insights import (
|
|
1476
1476
|
compute_insights,
|
|
1477
|
+
get_cross_session_repetition_insight,
|
|
1477
1478
|
get_effectiveness_insight,
|
|
1478
1479
|
get_similar_prompts_insight,
|
|
1479
1480
|
)
|
|
@@ -1487,12 +1488,14 @@ def insights(
|
|
|
1487
1488
|
# Expanded sections
|
|
1488
1489
|
eff_data = get_effectiveness_insight(db, source=source)
|
|
1489
1490
|
sim_data = get_similar_prompts_insight(db, source=source)
|
|
1491
|
+
rep_data = get_cross_session_repetition_insight(db, source=source)
|
|
1490
1492
|
|
|
1491
1493
|
if json_output:
|
|
1492
1494
|
import json as json_mod
|
|
1493
1495
|
|
|
1494
1496
|
result["effectiveness"] = eff_data
|
|
1495
1497
|
result["similar_prompts"] = sim_data
|
|
1498
|
+
result["cross_session_repetition"] = rep_data
|
|
1496
1499
|
typer.echo(json_mod.dumps(result, indent=2))
|
|
1497
1500
|
else:
|
|
1498
1501
|
from reprompt.core.suggestions import get_suggestion
|
|
@@ -1516,6 +1519,15 @@ def insights(
|
|
|
1516
1519
|
' [dim]\u2192 Try: reprompt template save "..." (reuse instead of rewrite)[/dim]'
|
|
1517
1520
|
)
|
|
1518
1521
|
|
|
1522
|
+
if rep_data:
|
|
1523
|
+
rate_pct = f"{rep_data['repetition_rate'] * 100:.0f}%"
|
|
1524
|
+
n = rep_data["total_recurring_topics"]
|
|
1525
|
+
console.print("\n [bold]Cross-Session Repetition[/bold]")
|
|
1526
|
+
console.print(f" {rate_pct} of prompts recur across sessions ({n} topics)")
|
|
1527
|
+
for t in rep_data["top_topics"]:
|
|
1528
|
+
console.print(f' "{t["canonical_text"]}" \u2014 {t["session_count"]} sessions')
|
|
1529
|
+
console.print(" [dim]\u2192 Try: reprompt repetition (full analysis)[/dim]")
|
|
1530
|
+
|
|
1519
1531
|
hint = get_suggestion("insights")
|
|
1520
1532
|
if hint:
|
|
1521
1533
|
console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
|
|
@@ -1525,6 +1537,7 @@ def insights(
|
|
|
1525
1537
|
|
|
1526
1538
|
result["effectiveness"] = eff_data
|
|
1527
1539
|
result["similar_prompts"] = sim_data
|
|
1540
|
+
result["cross_session_repetition"] = rep_data
|
|
1528
1541
|
_copy_to_clip(json_mod.dumps(result, indent=2), quiet=json_output)
|
|
1529
1542
|
|
|
1530
1543
|
|
|
@@ -1974,6 +1987,44 @@ def sessions(
|
|
|
1974
1987
|
_copy_to_clip(copy_text, quiet=json_output)
|
|
1975
1988
|
|
|
1976
1989
|
|
|
1990
|
+
@app.command(rich_help_panel="Analyze")
|
|
1991
|
+
def repetition(
|
|
1992
|
+
last: int = typer.Option(500, "--last", help="Analyze N most recent unique prompts"),
|
|
1993
|
+
source: str = typer.Option(
|
|
1994
|
+
None, "--source", "-s", help="Filter by source (e.g. claude-code, cursor)"
|
|
1995
|
+
),
|
|
1996
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
1997
|
+
copy: bool = typer.Option(False, "--copy", help="Copy result to clipboard"),
|
|
1998
|
+
) -> None:
|
|
1999
|
+
"""Detect recurring prompts across different sessions."""
|
|
2000
|
+
import json as json_mod
|
|
2001
|
+
from dataclasses import asdict
|
|
2002
|
+
|
|
2003
|
+
from reprompt.config import Settings
|
|
2004
|
+
from reprompt.core.repetition import analyze_repetition
|
|
2005
|
+
from reprompt.output.repetition_terminal import render_repetition_report
|
|
2006
|
+
from reprompt.storage.db import PromptDB
|
|
2007
|
+
|
|
2008
|
+
settings = Settings()
|
|
2009
|
+
db = PromptDB(settings.db_path)
|
|
2010
|
+
report = analyze_repetition(db, source=source, limit=last)
|
|
2011
|
+
|
|
2012
|
+
if json_output:
|
|
2013
|
+
typer.echo(json_mod.dumps(asdict(report), indent=2, default=str))
|
|
2014
|
+
else:
|
|
2015
|
+
typer.echo(render_repetition_report(report), nl=False)
|
|
2016
|
+
|
|
2017
|
+
from reprompt.core.suggestions import get_suggestion
|
|
2018
|
+
|
|
2019
|
+
hint = get_suggestion("repetition")
|
|
2020
|
+
if hint:
|
|
2021
|
+
console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
|
|
2022
|
+
|
|
2023
|
+
if copy:
|
|
2024
|
+
copy_text = json_mod.dumps(asdict(report), indent=2, default=str)
|
|
2025
|
+
_copy_to_clip(copy_text, quiet=json_output)
|
|
2026
|
+
|
|
2027
|
+
|
|
1977
2028
|
@app.command(rich_help_panel="Analyze")
|
|
1978
2029
|
def agent(
|
|
1979
2030
|
last: int = typer.Option(5, "--last", help="Analyze N most recent sessions"),
|
|
@@ -293,3 +293,29 @@ def get_similar_prompts_insight(
|
|
|
293
293
|
"total_clusters": len(clusters),
|
|
294
294
|
"total_clustered_prompts": sum(c["size"] for c in clusters),
|
|
295
295
|
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_cross_session_repetition_insight(
|
|
299
|
+
db: PromptDB,
|
|
300
|
+
source: str | None = None,
|
|
301
|
+
) -> dict[str, Any] | None:
|
|
302
|
+
"""Return cross-session repetition summary, or None if no recurring topics."""
|
|
303
|
+
from reprompt.core.repetition import analyze_repetition
|
|
304
|
+
|
|
305
|
+
report = analyze_repetition(db, source=source, limit=500)
|
|
306
|
+
|
|
307
|
+
if not report.recurring_topics:
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
"repetition_rate": report.repetition_rate,
|
|
312
|
+
"top_topics": [
|
|
313
|
+
{
|
|
314
|
+
"canonical_text": t.canonical_text[:80],
|
|
315
|
+
"session_count": t.session_count,
|
|
316
|
+
"total_matches": t.total_matches,
|
|
317
|
+
}
|
|
318
|
+
for t in report.recurring_topics[:3]
|
|
319
|
+
],
|
|
320
|
+
"total_recurring_topics": len(report.recurring_topics),
|
|
321
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Cross-session prompt repetition detection.
|
|
2
|
+
|
|
3
|
+
Identifies recurring topics asked across different AI coding sessions
|
|
4
|
+
using TF-IDF + containment similarity clustering. All analysis is
|
|
5
|
+
rule-based (zero LLM).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from reprompt.storage.db import PromptDB
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class RecurringTopic:
|
|
18
|
+
"""A topic that recurs across multiple sessions."""
|
|
19
|
+
|
|
20
|
+
canonical_text: str
|
|
21
|
+
session_count: int
|
|
22
|
+
total_matches: int
|
|
23
|
+
session_ids: list[str] = field(default_factory=list)
|
|
24
|
+
earliest: str = ""
|
|
25
|
+
latest: str = ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RepetitionReport:
|
|
30
|
+
"""Result of cross-session repetition analysis."""
|
|
31
|
+
|
|
32
|
+
total_prompts_analyzed: int = 0
|
|
33
|
+
cross_session_matches: int = 0
|
|
34
|
+
repetition_rate: float = 0.0 # cross_session_matches / total
|
|
35
|
+
recurring_topics: list[RecurringTopic] = field(default_factory=list)
|
|
36
|
+
total_sessions: int = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def analyze_repetition(
|
|
40
|
+
db: PromptDB,
|
|
41
|
+
source: str | None = None,
|
|
42
|
+
limit: int = 500,
|
|
43
|
+
threshold: float = 0.75,
|
|
44
|
+
) -> RepetitionReport:
|
|
45
|
+
"""Detect recurring prompts across different sessions.
|
|
46
|
+
|
|
47
|
+
Reuses merge_view.build_clusters() for similarity, then filters
|
|
48
|
+
to clusters spanning 2+ distinct sessions.
|
|
49
|
+
"""
|
|
50
|
+
from reprompt.core.merge_view import build_clusters
|
|
51
|
+
|
|
52
|
+
all_prompts = db.get_all_prompts(source=source)
|
|
53
|
+
|
|
54
|
+
# Filter to unique prompts only
|
|
55
|
+
unique = [p for p in all_prompts if p.get("duplicate_of") is None]
|
|
56
|
+
|
|
57
|
+
if not unique:
|
|
58
|
+
return RepetitionReport()
|
|
59
|
+
|
|
60
|
+
# Limit to most recent N (by id desc), then reverse for chronological
|
|
61
|
+
unique.sort(key=lambda p: p.get("id", 0), reverse=True)
|
|
62
|
+
unique = unique[:limit]
|
|
63
|
+
unique.reverse()
|
|
64
|
+
|
|
65
|
+
# Build lookup: text → prompt dict (safe due to hash uniqueness)
|
|
66
|
+
text_to_prompt: dict[str, dict[str, Any]] = {}
|
|
67
|
+
for p in unique:
|
|
68
|
+
text_to_prompt[p["text"]] = p
|
|
69
|
+
|
|
70
|
+
texts = [p["text"] for p in unique]
|
|
71
|
+
timestamps = [p.get("timestamp", "") for p in unique]
|
|
72
|
+
all_session_ids = {p.get("session_id") or "unknown" for p in unique}
|
|
73
|
+
|
|
74
|
+
# Build clusters using existing infrastructure
|
|
75
|
+
clusters = build_clusters(texts, timestamps, threshold=threshold)
|
|
76
|
+
|
|
77
|
+
# Filter to cross-session clusters
|
|
78
|
+
recurring: list[RecurringTopic] = []
|
|
79
|
+
total_cross_matches = 0
|
|
80
|
+
|
|
81
|
+
for cluster in clusters:
|
|
82
|
+
# Collect all texts in cluster (canonical + members)
|
|
83
|
+
cluster_texts = [cluster["canonical"]["text"]]
|
|
84
|
+
cluster_texts.extend(m["text"] for m in cluster["members"])
|
|
85
|
+
|
|
86
|
+
# Map to session_ids
|
|
87
|
+
sids: list[str] = []
|
|
88
|
+
cluster_timestamps: list[str] = []
|
|
89
|
+
for t in cluster_texts:
|
|
90
|
+
prompt = text_to_prompt.get(t)
|
|
91
|
+
if prompt:
|
|
92
|
+
sids.append(prompt.get("session_id") or "unknown")
|
|
93
|
+
cluster_timestamps.append(prompt.get("timestamp", ""))
|
|
94
|
+
|
|
95
|
+
distinct_sessions = sorted(set(sids))
|
|
96
|
+
if len(distinct_sessions) < 2:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Build timestamps for range
|
|
100
|
+
valid_ts = sorted(t for t in cluster_timestamps if t)
|
|
101
|
+
earliest = valid_ts[0] if valid_ts else ""
|
|
102
|
+
latest = valid_ts[-1] if valid_ts else ""
|
|
103
|
+
|
|
104
|
+
recurring.append(
|
|
105
|
+
RecurringTopic(
|
|
106
|
+
canonical_text=cluster["canonical"]["text"],
|
|
107
|
+
session_count=len(distinct_sessions),
|
|
108
|
+
total_matches=len(cluster_texts),
|
|
109
|
+
session_ids=distinct_sessions,
|
|
110
|
+
earliest=earliest,
|
|
111
|
+
latest=latest,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
total_cross_matches += len(cluster_texts)
|
|
115
|
+
|
|
116
|
+
# Sort by session_count desc, then total_matches desc
|
|
117
|
+
recurring.sort(key=lambda t: (-t.session_count, -t.total_matches))
|
|
118
|
+
|
|
119
|
+
total = len(unique)
|
|
120
|
+
rate = total_cross_matches / total if total > 0 else 0.0
|
|
121
|
+
|
|
122
|
+
return RepetitionReport(
|
|
123
|
+
total_prompts_analyzed=total,
|
|
124
|
+
cross_session_matches=total_cross_matches,
|
|
125
|
+
repetition_rate=round(rate, 3),
|
|
126
|
+
recurring_topics=recurring,
|
|
127
|
+
total_sessions=len(all_session_ids),
|
|
128
|
+
)
|
|
@@ -82,9 +82,7 @@ def _detect_frustration(turns: list[ConversationTurn]) -> FrustrationSignals:
|
|
|
82
82
|
escalation = second_rate > first_rate * 1.5 and second_rate > 0.2
|
|
83
83
|
|
|
84
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
|
-
)
|
|
85
|
+
stall_turns = sum(1 for t in asst_turns if t.tool_calls == 0 and len(t.text.strip()) < 50)
|
|
88
86
|
|
|
89
87
|
return FrustrationSignals(
|
|
90
88
|
abandonment=abandonment,
|
|
@@ -18,6 +18,9 @@ SUGGESTIONS: dict[str, str] = {
|
|
|
18
18
|
"sessions": (
|
|
19
19
|
"reprompt sessions --detail <id> (deep-dive) · reprompt agent (error loop analysis)"
|
|
20
20
|
),
|
|
21
|
+
"repetition": (
|
|
22
|
+
'reprompt template save "..." (reuse patterns) · reprompt insights (all patterns)'
|
|
23
|
+
),
|
|
21
24
|
"template": "reprompt insights (see which patterns work best)",
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Rich terminal rendering for cross-session repetition analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from io import StringIO
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from reprompt.core.repetition import RepetitionReport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def render_repetition_report(report: RepetitionReport) -> str:
|
|
15
|
+
"""Render repetition report as formatted terminal output."""
|
|
16
|
+
console = Console(record=True, width=100, file=StringIO())
|
|
17
|
+
|
|
18
|
+
if not report.recurring_topics:
|
|
19
|
+
console.print("[dim]No cross-session repetition detected.[/dim]")
|
|
20
|
+
console.print(
|
|
21
|
+
"This means your prompts across sessions are unique — no recurring patterns found."
|
|
22
|
+
)
|
|
23
|
+
return console.export_text()
|
|
24
|
+
|
|
25
|
+
rate_pct = f"{report.repetition_rate * 100:.0f}%"
|
|
26
|
+
header = (
|
|
27
|
+
f"Prompts: {report.total_prompts_analyzed} | "
|
|
28
|
+
f"Sessions: {report.total_sessions} | "
|
|
29
|
+
f"Repetition Rate: {rate_pct}"
|
|
30
|
+
)
|
|
31
|
+
console.print(Panel(header, title="Cross-Session Repetition", border_style="cyan"))
|
|
32
|
+
|
|
33
|
+
# Table of recurring topics
|
|
34
|
+
table = Table(show_header=True, header_style="bold", padding=(0, 1))
|
|
35
|
+
table.add_column("Topic", max_width=45)
|
|
36
|
+
table.add_column("Sessions", justify="right", width=8)
|
|
37
|
+
table.add_column("Matches", justify="right", width=7)
|
|
38
|
+
table.add_column("Range", max_width=25)
|
|
39
|
+
|
|
40
|
+
for topic in report.recurring_topics[:10]:
|
|
41
|
+
text = topic.canonical_text
|
|
42
|
+
if len(text) > 45:
|
|
43
|
+
text = text[:42] + "..."
|
|
44
|
+
|
|
45
|
+
date_range = ""
|
|
46
|
+
if topic.earliest and topic.latest:
|
|
47
|
+
start = topic.earliest[:10]
|
|
48
|
+
end = topic.latest[:10]
|
|
49
|
+
date_range = f"{start} \u2192 {end}" if start != end else start
|
|
50
|
+
|
|
51
|
+
table.add_row(
|
|
52
|
+
text,
|
|
53
|
+
str(topic.session_count),
|
|
54
|
+
str(topic.total_matches),
|
|
55
|
+
date_range,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
console.print(table)
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
return console.export_text()
|
|
@@ -49,9 +49,7 @@ def render_sessions_table(sessions: list[dict[str, Any]]) -> str:
|
|
|
49
49
|
|
|
50
50
|
if not sessions:
|
|
51
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
|
-
)
|
|
52
|
+
console.print("Run [bold cyan]reprompt scan[/bold cyan] to import and score sessions.")
|
|
55
53
|
return console.export_text()
|
|
56
54
|
|
|
57
55
|
# Compute avg quality
|
|
@@ -59,11 +57,7 @@ def render_sessions_table(sessions: list[dict[str, Any]]) -> str:
|
|
|
59
57
|
avg_q = sum(s["quality_score"] for s in scored) / len(scored) if scored else 0
|
|
60
58
|
|
|
61
59
|
# Header
|
|
62
|
-
header = (
|
|
63
|
-
f"Sessions: {len(sessions)} | "
|
|
64
|
-
f"Scored: {len(scored)} | "
|
|
65
|
-
f"Avg Quality: {avg_q:.0f}/100"
|
|
66
|
-
)
|
|
60
|
+
header = f"Sessions: {len(sessions)} | Scored: {len(scored)} | Avg Quality: {avg_q:.0f}/100"
|
|
67
61
|
console.print(Panel(header, title="Session Quality", border_style="cyan"))
|
|
68
62
|
|
|
69
63
|
# Table
|
|
@@ -964,6 +964,23 @@ class PromptDB:
|
|
|
964
964
|
finally:
|
|
965
965
|
conn.close()
|
|
966
966
|
|
|
967
|
+
def get_recent_scores(self, limit: int = 50) -> list[float]:
|
|
968
|
+
"""Return recent prompt scores ordered by timestamp (newest first)."""
|
|
969
|
+
conn = self._conn()
|
|
970
|
+
try:
|
|
971
|
+
rows = conn.execute(
|
|
972
|
+
"""SELECT pf.overall_score
|
|
973
|
+
FROM prompt_features pf
|
|
974
|
+
JOIN prompts p ON pf.prompt_hash = p.hash
|
|
975
|
+
WHERE pf.overall_score IS NOT NULL
|
|
976
|
+
ORDER BY p.timestamp DESC
|
|
977
|
+
LIMIT ?""",
|
|
978
|
+
(limit,),
|
|
979
|
+
).fetchall()
|
|
980
|
+
return [r["overall_score"] for r in rows]
|
|
981
|
+
finally:
|
|
982
|
+
conn.close()
|
|
983
|
+
|
|
967
984
|
def get_all_features(self, source: str | None = None) -> list[dict[str, Any]]:
|
|
968
985
|
"""Return all stored feature vectors, optionally filtered by source."""
|
|
969
986
|
conn = self._conn()
|