reprompt-cli 1.8.0__tar.gz → 1.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/PKG-INFO +1 -1
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/pyproject.toml +1 -1
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/__init__.py +1 -1
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/cli.py +51 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/insights.py +26 -0
- reprompt_cli-1.8.1/src/reprompt/core/repetition.py +128 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/session_quality.py +1 -3
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/suggestions.py +3 -0
- reprompt_cli-1.8.1/src/reprompt/output/repetition_terminal.py +61 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/sessions_terminal.py +2 -8
- reprompt_cli-1.8.1/tests/test_repetition.py +124 -0
- reprompt_cli-1.8.1/tests/test_repetition_cli.py +102 -0
- reprompt_cli-1.8.1/tests/test_repetition_output.py +76 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_suggestions.py +9 -2
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/uv.lock +1 -1
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.editorconfig +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/dependabot.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/workflows/ci.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/workflows/publish.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.gitignore +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.pre-commit-config.yaml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.pre-commit-hooks.yaml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.testmondata-shm +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.testmondata-wal +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/CHANGELOG.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/CODE_OF_CONDUCT.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/CONTRIBUTING.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/LICENSE +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/README.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/SECURITY.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/action.yml +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/demo.gif +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-128.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-16.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-256.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-32.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-48.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-512.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-96.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon.svg +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-128.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-16.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-256.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-32.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-48.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-512.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-96.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon.svg +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-128.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-16.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-256.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-32.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-48.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-512.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-96.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon.svg +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-128.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-16.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-256.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-32.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-48.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-512.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-96.png +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon.svg +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/generate.sh +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/scripts/generate_demo_data.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/aider.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/base.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/chatgpt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/claude_chat.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/claude_code.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/cline.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/codex.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/cursor.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/filters.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/gemini.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/openclaw.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/handler.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/host.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/manifest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/protocol.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/commands/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/commands/telemetry.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/commands/wrapped.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/config.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/agent.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/analyzer.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/compress.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/conversation.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/cost.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/dashboard.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/dedup.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/digest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/distill.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/effectiveness.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/extractors.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/extractors_zh.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/lang_detect.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/library.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/lint.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/merge_view.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/models.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/persona.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/pipeline.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/privacy.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/privacy_scan.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/prompt_dna.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/recommend.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/scorer.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/segmenter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/session_meta.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/session_type.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/style.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/templates.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/timeutil.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/trends.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/wrapped.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/demo.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/base.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/local_embed.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/ollama.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/openai_embed.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/tfidf.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/mcp.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/mcp_main.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/agent_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/chartjs.min.js +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/compress_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/dashboard_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/distill_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/export.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/html_report.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/json_out.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/markdown.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/wrapped_html.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/wrapped_terminal.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/py.typed +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/sharing/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/sharing/client.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/sharing/clipboard.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/storage/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/storage/db.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/collector.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/consent.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/events.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/prompt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/queue.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/sender.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/__init__.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/conftest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/aider_chat_history.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/chatgpt_conversations.json +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/claude_chat_export.json +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/claude_session.jsonl +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/export/default_export.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/export/full_export.md +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/gemini_session.json +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/openclaw_session.jsonl +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_aider.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_chatgpt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_claude.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_claude_chat.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_cline.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_gemini.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_openclaw.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_agent.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_agent_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_analyzer.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_handler.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_integration.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_manifest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_protocol.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cli_deprecations.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cli_library_effectiveness.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_clipboard.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_codex_adapter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compare_best_worst.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress_dna.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress_html.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress_insights.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_config.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_conversation.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_copy_flag.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cost.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_coverage_boost.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cursor_adapter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_dashboard.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db_digest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db_effectiveness.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db_session_quality.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db_trends.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_dedup.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_demo.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_deprecated_commands.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_digest.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_digest_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_distill.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_distill_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_distill_weights.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_effectiveness.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_embeddings_local.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_embeddings_ollama.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_embeddings_openai.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_empty_state.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_export.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_export_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_export_snapshot.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_extractors.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_extractors_routing.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_extractors_zh.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_extractors_zh_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_html_report.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_import_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_import_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_insights.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_insights_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_insights_expanded.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_install_hook.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_lang_detect.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_library.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_lint.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_lint_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_markdown.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_mcp.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_merge_view.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_models.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_output.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_parse_conversation_base.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_parse_conversation_chatgpt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_parse_conversation_claude.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_persona.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_pipeline.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy_output.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy_scan.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_prompt_dna.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_public_api.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_recommend.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_schema_version.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_score_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_scorer.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_segmenter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_session_quality.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_session_type.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_sessions_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_sessions_output.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_share_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_sharing_client.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_source_filter.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_style.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_style_trends.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_collector.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_consent.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_events.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_prompt.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_queue.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_sender.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_template_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_templates.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_timeutil.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_trends.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_trends_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_use_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped_cli.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped_e2e.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped_html.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped_output.py +0 -0
- {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/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.8.
|
|
3
|
+
Version: 1.8.1
|
|
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
|
|
@@ -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
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Tests for cross-session prompt repetition detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from reprompt.core.repetition import analyze_repetition
|
|
10
|
+
from reprompt.storage.db import PromptDB
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def db(tmp_path: Path) -> PromptDB:
|
|
15
|
+
return PromptDB(tmp_path / "test.db")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _insert(db: PromptDB, text: str, session_id: str, source: str = "claude-code") -> None:
|
|
19
|
+
db.insert_prompt(text, source=source, session_id=session_id, timestamp="2026-03-28T10:00:00Z")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestAnalyzeRepetition:
|
|
23
|
+
def test_empty_db_returns_zero(self, db: PromptDB):
|
|
24
|
+
report = analyze_repetition(db)
|
|
25
|
+
assert report.total_prompts_analyzed == 0
|
|
26
|
+
assert report.repetition_rate == 0.0
|
|
27
|
+
assert report.recurring_topics == []
|
|
28
|
+
|
|
29
|
+
def test_single_session_no_cross_session(self, db: PromptDB):
|
|
30
|
+
"""Similar prompts in the same session don't count as cross-session."""
|
|
31
|
+
_insert(db, "fix the authentication bug in login.py", "s1")
|
|
32
|
+
_insert(db, "fix the authentication issue in login module", "s1")
|
|
33
|
+
_insert(db, "fix the auth problem in the login file", "s1")
|
|
34
|
+
report = analyze_repetition(db)
|
|
35
|
+
assert report.repetition_rate == 0.0
|
|
36
|
+
assert len(report.recurring_topics) == 0
|
|
37
|
+
|
|
38
|
+
def test_cross_session_detected(self, db: PromptDB):
|
|
39
|
+
"""Similar prompts across sessions form a recurring topic."""
|
|
40
|
+
_insert(db, "fix the authentication bug in login.py please", "s1")
|
|
41
|
+
_insert(db, "fix the authentication bug in login.py now", "s2")
|
|
42
|
+
report = analyze_repetition(db)
|
|
43
|
+
assert len(report.recurring_topics) >= 1
|
|
44
|
+
topic = report.recurring_topics[0]
|
|
45
|
+
assert topic.session_count >= 2
|
|
46
|
+
assert topic.total_matches >= 2
|
|
47
|
+
assert report.repetition_rate > 0
|
|
48
|
+
|
|
49
|
+
def test_unrelated_prompts_no_match(self, db: PromptDB):
|
|
50
|
+
"""Completely different prompts don't cluster."""
|
|
51
|
+
_insert(db, "fix the authentication bug in login.py", "s1")
|
|
52
|
+
_insert(db, "add pagination to the user list API endpoint", "s2")
|
|
53
|
+
report = analyze_repetition(db)
|
|
54
|
+
assert report.repetition_rate == 0.0
|
|
55
|
+
|
|
56
|
+
def test_three_sessions_same_topic(self, db: PromptDB):
|
|
57
|
+
"""Topic spanning 3 sessions gets session_count=3."""
|
|
58
|
+
_insert(db, "fix the authentication bug in login.py", "s1")
|
|
59
|
+
_insert(db, "fix the authentication issue in login module", "s2")
|
|
60
|
+
_insert(db, "fix auth problem in the login file", "s3")
|
|
61
|
+
report = analyze_repetition(db)
|
|
62
|
+
if report.recurring_topics:
|
|
63
|
+
topic = report.recurring_topics[0]
|
|
64
|
+
assert topic.session_count >= 2 # at least 2, ideally 3
|
|
65
|
+
|
|
66
|
+
def test_duplicate_of_excluded(self, db: PromptDB):
|
|
67
|
+
"""Prompts marked as duplicates should be excluded."""
|
|
68
|
+
_insert(db, "fix the authentication bug in login.py", "s1")
|
|
69
|
+
_insert(db, "fix the authentication bug in login.py copy", "s2")
|
|
70
|
+
# Mark second as duplicate
|
|
71
|
+
conn = db._conn()
|
|
72
|
+
try:
|
|
73
|
+
conn.execute("UPDATE prompts SET duplicate_of = 1 WHERE session_id = 's2'")
|
|
74
|
+
conn.commit()
|
|
75
|
+
finally:
|
|
76
|
+
conn.close()
|
|
77
|
+
report = analyze_repetition(db)
|
|
78
|
+
assert report.total_prompts_analyzed == 1 # only the non-duplicate
|
|
79
|
+
|
|
80
|
+
def test_limit_caps_analysis(self, db: PromptDB):
|
|
81
|
+
"""Limit parameter restricts how many prompts are analyzed."""
|
|
82
|
+
for i in range(10):
|
|
83
|
+
_insert(db, f"unique prompt number {i} about topic {i}", f"s{i}")
|
|
84
|
+
report = analyze_repetition(db, limit=5)
|
|
85
|
+
assert report.total_prompts_analyzed == 5
|
|
86
|
+
|
|
87
|
+
def test_sorted_by_session_count(self, db: PromptDB):
|
|
88
|
+
"""Topics sorted by session_count descending."""
|
|
89
|
+
# Topic A: 3 sessions
|
|
90
|
+
_insert(db, "fix the authentication bug in login.py", "s1")
|
|
91
|
+
_insert(db, "fix the authentication issue in login module", "s2")
|
|
92
|
+
_insert(db, "fix the auth problem in login file", "s3")
|
|
93
|
+
# Topic B: 2 sessions (different topic)
|
|
94
|
+
_insert(db, "add comprehensive unit tests for the payment API", "s4")
|
|
95
|
+
_insert(db, "add unit tests for the payment API endpoint", "s5")
|
|
96
|
+
report = analyze_repetition(db)
|
|
97
|
+
if len(report.recurring_topics) >= 2:
|
|
98
|
+
assert (
|
|
99
|
+
report.recurring_topics[0].session_count >= report.recurring_topics[1].session_count
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def test_source_filter(self, db: PromptDB):
|
|
103
|
+
"""Source filter limits analysis to specific adapter."""
|
|
104
|
+
_insert(db, "fix the authentication bug in login.py", "s1", source="claude-code")
|
|
105
|
+
_insert(db, "fix the authentication issue in login", "s2", source="cursor")
|
|
106
|
+
report = analyze_repetition(db, source="claude-code")
|
|
107
|
+
assert report.total_prompts_analyzed == 1
|
|
108
|
+
|
|
109
|
+
def test_total_sessions_counted(self, db: PromptDB):
|
|
110
|
+
"""Total sessions reflects distinct session count in scope."""
|
|
111
|
+
_insert(db, "prompt one about topic alpha", "s1")
|
|
112
|
+
_insert(db, "prompt two about topic beta", "s2")
|
|
113
|
+
_insert(db, "prompt three about topic gamma", "s3")
|
|
114
|
+
report = analyze_repetition(db)
|
|
115
|
+
assert report.total_sessions == 3
|
|
116
|
+
|
|
117
|
+
def test_report_fields_present(self, db: PromptDB):
|
|
118
|
+
"""All RepetitionReport fields are accessible."""
|
|
119
|
+
report = analyze_repetition(db)
|
|
120
|
+
assert isinstance(report.total_prompts_analyzed, int)
|
|
121
|
+
assert isinstance(report.cross_session_matches, int)
|
|
122
|
+
assert isinstance(report.repetition_rate, float)
|
|
123
|
+
assert isinstance(report.recurring_topics, list)
|
|
124
|
+
assert isinstance(report.total_sessions, int)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tests for `reprompt repetition` CLI command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
from reprompt.cli import app
|
|
11
|
+
|
|
12
|
+
runner = CliRunner()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _seed_db(tmp_path: Path) -> Path:
|
|
16
|
+
"""Create a DB with cross-session similar prompts."""
|
|
17
|
+
from reprompt.storage.db import PromptDB
|
|
18
|
+
|
|
19
|
+
db_path = tmp_path / "test.db"
|
|
20
|
+
db = PromptDB(db_path)
|
|
21
|
+
|
|
22
|
+
# Topic A: auth bug across 2 sessions
|
|
23
|
+
db.insert_prompt(
|
|
24
|
+
"fix the authentication bug in login.py please",
|
|
25
|
+
source="claude-code",
|
|
26
|
+
session_id="s1",
|
|
27
|
+
timestamp="2026-03-01T10:00:00Z",
|
|
28
|
+
)
|
|
29
|
+
db.insert_prompt(
|
|
30
|
+
"fix the authentication bug in login.py now",
|
|
31
|
+
source="claude-code",
|
|
32
|
+
session_id="s2",
|
|
33
|
+
timestamp="2026-03-15T10:00:00Z",
|
|
34
|
+
)
|
|
35
|
+
# Unrelated prompt
|
|
36
|
+
db.insert_prompt(
|
|
37
|
+
"add pagination to the user list API endpoint",
|
|
38
|
+
source="claude-code",
|
|
39
|
+
session_id="s3",
|
|
40
|
+
timestamp="2026-03-20T10:00:00Z",
|
|
41
|
+
)
|
|
42
|
+
return db_path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_repetition_no_data(tmp_path, monkeypatch):
|
|
46
|
+
db_path = tmp_path / "empty.db"
|
|
47
|
+
monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
|
|
48
|
+
result = runner.invoke(app, ["repetition"])
|
|
49
|
+
assert result.exit_code == 0
|
|
50
|
+
assert "no cross-session" in result.output.lower()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_repetition_no_data_json(tmp_path, monkeypatch):
|
|
54
|
+
db_path = tmp_path / "empty.db"
|
|
55
|
+
monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
|
|
56
|
+
result = runner.invoke(app, ["repetition", "--json"])
|
|
57
|
+
assert result.exit_code == 0
|
|
58
|
+
data = json.loads(result.output)
|
|
59
|
+
assert data["repetition_rate"] == 0.0
|
|
60
|
+
assert data["recurring_topics"] == []
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_repetition_with_data(tmp_path, monkeypatch):
|
|
64
|
+
db_path = _seed_db(tmp_path)
|
|
65
|
+
monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
|
|
66
|
+
result = runner.invoke(app, ["repetition"])
|
|
67
|
+
assert result.exit_code == 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_repetition_json_output(tmp_path, monkeypatch):
|
|
71
|
+
db_path = _seed_db(tmp_path)
|
|
72
|
+
monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
|
|
73
|
+
result = runner.invoke(app, ["repetition", "--json"])
|
|
74
|
+
assert result.exit_code == 0
|
|
75
|
+
data = json.loads(result.output)
|
|
76
|
+
assert "repetition_rate" in data
|
|
77
|
+
assert "recurring_topics" in data
|
|
78
|
+
assert "total_prompts_analyzed" in data
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_repetition_source_filter(tmp_path, monkeypatch):
|
|
82
|
+
db_path = _seed_db(tmp_path)
|
|
83
|
+
monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
|
|
84
|
+
result = runner.invoke(app, ["repetition", "--source", "nonexistent", "--json"])
|
|
85
|
+
assert result.exit_code == 0
|
|
86
|
+
data = json.loads(result.output)
|
|
87
|
+
assert data["total_prompts_analyzed"] == 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_repetition_last_option(tmp_path, monkeypatch):
|
|
91
|
+
db_path = _seed_db(tmp_path)
|
|
92
|
+
monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
|
|
93
|
+
result = runner.invoke(app, ["repetition", "--last", "2", "--json"])
|
|
94
|
+
assert result.exit_code == 0
|
|
95
|
+
data = json.loads(result.output)
|
|
96
|
+
assert data["total_prompts_analyzed"] <= 2
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_repetition_help():
|
|
100
|
+
result = runner.invoke(app, ["repetition", "--help"])
|
|
101
|
+
assert result.exit_code == 0
|
|
102
|
+
assert "recurring" in result.output.lower() or "repetition" in result.output.lower()
|