reprompt-cli 1.8.1__tar.gz → 1.9.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.1 → reprompt_cli-1.9.1}/PKG-INFO +1 -1
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/pyproject.toml +1 -1
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/__init__.py +1 -1
- reprompt_cli-1.9.1/src/reprompt/bridge/handler.py +234 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/cli.py +12 -6
- reprompt_cli-1.9.1/src/reprompt/core/lint.py +242 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/storage/db.py +17 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_e2e.py +19 -19
- reprompt_cli-1.9.1/tests/test_bridge_handler.py +354 -0
- reprompt_cli-1.9.1/tests/test_lint.py +237 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/uv.lock +1 -1
- reprompt_cli-1.8.1/src/reprompt/bridge/handler.py +0 -100
- reprompt_cli-1.8.1/src/reprompt/core/lint.py +0 -112
- reprompt_cli-1.8.1/tests/test_bridge_handler.py +0 -118
- reprompt_cli-1.8.1/tests/test_lint.py +0 -81
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.editorconfig +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/dependabot.yml +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/workflows/ci.yml +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/workflows/publish.yml +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.gitignore +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.pre-commit-config.yaml +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.pre-commit-hooks.yaml +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.testmondata-shm +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.testmondata-wal +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/CHANGELOG.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/CODE_OF_CONDUCT.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/CONTRIBUTING.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/LICENSE +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/README.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/SECURITY.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/action.yml +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/demo.gif +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-128.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-16.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-256.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-32.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-48.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-512.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-96.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon.svg +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-128.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-16.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-256.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-32.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-48.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-512.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-96.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon.svg +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-128.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-16.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-256.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-32.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-48.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-512.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-96.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon.svg +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-128.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-16.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-256.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-32.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-48.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-512.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-96.png +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon.svg +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/generate.sh +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/scripts/generate_demo_data.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/aider.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/base.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/chatgpt.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/claude_chat.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/claude_code.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/cline.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/codex.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/cursor.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/filters.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/gemini.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/openclaw.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/bridge/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/bridge/host.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/bridge/manifest.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/bridge/protocol.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/commands/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/commands/telemetry.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/commands/wrapped.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/config.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/agent.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/analyzer.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/compress.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/conversation.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/cost.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/dashboard.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/dedup.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/digest.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/distill.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/effectiveness.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/extractors.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/extractors_zh.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/insights.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/lang_detect.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/library.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/merge_view.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/models.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/persona.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/pipeline.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/privacy.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/privacy_scan.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/prompt_dna.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/recommend.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/repetition.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/scorer.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/segmenter.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/session_meta.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/session_quality.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/session_type.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/style.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/suggestions.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/templates.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/timeutil.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/trends.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/wrapped.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/demo.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/base.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/local_embed.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/ollama.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/openai_embed.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/tfidf.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/mcp.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/mcp_main.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/agent_terminal.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/chartjs.min.js +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/compress_terminal.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/dashboard_terminal.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/distill_terminal.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/export.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/html_report.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/json_out.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/markdown.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/repetition_terminal.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/sessions_terminal.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/terminal.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/wrapped_html.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/wrapped_terminal.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/py.typed +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/sharing/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/sharing/client.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/sharing/clipboard.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/storage/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/collector.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/consent.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/events.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/prompt.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/queue.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/sender.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/__init__.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/conftest.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/aider_chat_history.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/chatgpt_conversations.json +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/claude_chat_export.json +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/claude_session.jsonl +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/export/default_export.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/export/full_export.md +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/gemini_session.json +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/openclaw_session.jsonl +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_aider.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_chatgpt.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_claude.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_claude_chat.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_cline.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_gemini.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_openclaw.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_agent.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_agent_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_analyzer.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_integration.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_manifest.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_protocol.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cli_deprecations.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cli_library_effectiveness.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_clipboard.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_codex_adapter.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compare_best_worst.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress_dna.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress_html.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress_insights.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_config.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_conversation.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_copy_flag.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cost.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_coverage_boost.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cursor_adapter.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_dashboard.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db_digest.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db_effectiveness.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db_session_quality.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db_trends.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_dedup.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_demo.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_deprecated_commands.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_digest.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_digest_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_distill.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_distill_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_distill_weights.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_e2e.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_effectiveness.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_embeddings_local.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_embeddings_ollama.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_embeddings_openai.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_empty_state.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_export.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_export_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_export_snapshot.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_extractors.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_extractors_routing.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_extractors_zh.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_extractors_zh_e2e.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_html_report.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_import_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_import_e2e.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_insights.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_insights_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_insights_expanded.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_install_hook.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_lang_detect.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_library.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_lint_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_markdown.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_mcp.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_merge_view.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_models.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_output.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_parse_conversation_base.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_parse_conversation_chatgpt.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_parse_conversation_claude.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_persona.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_pipeline.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy_e2e.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy_output.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy_scan.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_prompt_dna.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_public_api.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_recommend.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_repetition.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_repetition_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_repetition_output.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_schema_version.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_score_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_scorer.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_segmenter.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_session_quality.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_session_type.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_sessions_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_sessions_output.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_share_e2e.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_sharing_client.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_source_filter.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_style.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_style_trends.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_suggestions.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_collector.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_consent.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_e2e.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_events.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_prompt.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_queue.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_sender.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_template_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_templates.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_timeutil.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_trends.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_trends_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_use_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped_cli.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped_e2e.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped_html.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped_output.py +0 -0
- {reprompt_cli-1.8.1 → reprompt_cli-1.9.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.
|
|
3
|
+
Version: 1.9.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
|
|
@@ -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 ""
|
|
@@ -820,13 +820,19 @@ def lint(
|
|
|
820
820
|
import json as json_mod
|
|
821
821
|
|
|
822
822
|
from reprompt.config import Settings
|
|
823
|
-
from reprompt.core.lint import format_lint_results, lint_prompts
|
|
823
|
+
from reprompt.core.lint import format_lint_results, lint_prompts, load_lint_config
|
|
824
824
|
from reprompt.core.pipeline import get_adapters
|
|
825
825
|
from reprompt.storage.db import PromptDB
|
|
826
826
|
|
|
827
827
|
settings = Settings()
|
|
828
828
|
db = PromptDB(settings.db_path)
|
|
829
829
|
|
|
830
|
+
# Load lint config from .reprompt.toml / pyproject.toml
|
|
831
|
+
lint_config = load_lint_config()
|
|
832
|
+
|
|
833
|
+
# CLI flags override config file
|
|
834
|
+
effective_threshold = score_threshold if score_threshold > 0 else lint_config.score_threshold
|
|
835
|
+
|
|
830
836
|
# Collect prompts from DB (already scanned)
|
|
831
837
|
rows = db.get_all_prompts()
|
|
832
838
|
texts = [r["text"] for r in rows]
|
|
@@ -858,11 +864,11 @@ def lint(
|
|
|
858
864
|
console.print("No prompts found. Run [bold]reprompt scan[/bold] first.")
|
|
859
865
|
raise typer.Exit(0)
|
|
860
866
|
|
|
861
|
-
violations = lint_prompts(texts)
|
|
867
|
+
violations = lint_prompts(texts, config=lint_config)
|
|
862
868
|
|
|
863
869
|
# Score threshold mode (CI integration)
|
|
864
870
|
score_data = None
|
|
865
|
-
if
|
|
871
|
+
if effective_threshold > 0:
|
|
866
872
|
from reprompt.core.extractors import extract_features
|
|
867
873
|
from reprompt.core.scorer import score_prompt
|
|
868
874
|
|
|
@@ -875,8 +881,8 @@ def lint(
|
|
|
875
881
|
"avg_score": round(avg_score, 1),
|
|
876
882
|
"min_score": min(scores) if scores else 0,
|
|
877
883
|
"max_score": max(scores) if scores else 0,
|
|
878
|
-
"threshold":
|
|
879
|
-
"pass": avg_score >=
|
|
884
|
+
"threshold": effective_threshold,
|
|
885
|
+
"pass": avg_score >= effective_threshold,
|
|
880
886
|
}
|
|
881
887
|
|
|
882
888
|
if json_output:
|
|
@@ -905,7 +911,7 @@ def lint(
|
|
|
905
911
|
console.print(
|
|
906
912
|
f"\n Score: avg {score_data['avg_score']}/100"
|
|
907
913
|
f" (min {score_data['min_score']}, max {score_data['max_score']})"
|
|
908
|
-
f" — threshold {
|
|
914
|
+
f" — threshold {effective_threshold} → {status}"
|
|
909
915
|
)
|
|
910
916
|
|
|
911
917
|
if copy:
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Prompt quality linting rules.
|
|
2
|
+
|
|
3
|
+
Checks prompts against configurable quality rules and returns violations.
|
|
4
|
+
Designed for CI integration — each rule produces a severity + message.
|
|
5
|
+
|
|
6
|
+
Configuration loaded from (highest priority wins):
|
|
7
|
+
1. CLI flags (--score-threshold, --strict)
|
|
8
|
+
2. .reprompt.toml in CWD or parents
|
|
9
|
+
3. [tool.reprompt.lint] in pyproject.toml
|
|
10
|
+
4. Built-in defaults
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
if sys.version_info >= (3, 11):
|
|
20
|
+
import tomllib
|
|
21
|
+
else:
|
|
22
|
+
try:
|
|
23
|
+
import tomllib
|
|
24
|
+
except ModuleNotFoundError: # Python 3.10
|
|
25
|
+
import tomli as tomllib # type: ignore[no-redefine]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LintViolation:
|
|
30
|
+
"""A single lint rule violation."""
|
|
31
|
+
|
|
32
|
+
rule: str
|
|
33
|
+
severity: str # "error" | "warning"
|
|
34
|
+
message: str
|
|
35
|
+
prompt_text: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class LintConfig:
|
|
40
|
+
"""Configuration for lint rules. Loaded from .reprompt.toml or pyproject.toml."""
|
|
41
|
+
|
|
42
|
+
# Rule thresholds (0 = disabled)
|
|
43
|
+
min_length: int = 20
|
|
44
|
+
short_prompt: int = 40
|
|
45
|
+
|
|
46
|
+
# Boolean rules (False = disabled)
|
|
47
|
+
vague_prompt: bool = True
|
|
48
|
+
debug_needs_reference: bool = True
|
|
49
|
+
|
|
50
|
+
# File extensions for debug-needs-reference
|
|
51
|
+
file_extensions: list[str] = field(
|
|
52
|
+
default_factory=lambda: [".py", ".ts", ".js", ".go", ".rs", ".java", ".rb", ".cpp", ".c"]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# CI score threshold (0 = disabled, set via --score-threshold or config)
|
|
56
|
+
score_threshold: int = 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# --- Default config ---
|
|
60
|
+
DEFAULT_CONFIG = LintConfig()
|
|
61
|
+
|
|
62
|
+
VAGUE_STARTERS = frozenset(
|
|
63
|
+
{"fix it", "fix this", "do it", "help me", "change it", "update it", "make it work"}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_lint_config(start_dir: Path | None = None) -> LintConfig:
|
|
68
|
+
"""Load lint config from .reprompt.toml or pyproject.toml, walking up from start_dir.
|
|
69
|
+
|
|
70
|
+
Returns DEFAULT_CONFIG if no config file found.
|
|
71
|
+
"""
|
|
72
|
+
if start_dir is None:
|
|
73
|
+
start_dir = Path.cwd()
|
|
74
|
+
|
|
75
|
+
# Walk up to find config files
|
|
76
|
+
current = start_dir.resolve()
|
|
77
|
+
for _ in range(20): # safety limit
|
|
78
|
+
# Check .reprompt.toml first (project-specific)
|
|
79
|
+
reprompt_toml = current / ".reprompt.toml"
|
|
80
|
+
if reprompt_toml.is_file():
|
|
81
|
+
return _parse_reprompt_toml(reprompt_toml)
|
|
82
|
+
|
|
83
|
+
# Check pyproject.toml
|
|
84
|
+
pyproject = current / "pyproject.toml"
|
|
85
|
+
if pyproject.is_file():
|
|
86
|
+
config = _parse_pyproject_toml(pyproject)
|
|
87
|
+
if config is not None:
|
|
88
|
+
return config
|
|
89
|
+
|
|
90
|
+
parent = current.parent
|
|
91
|
+
if parent == current:
|
|
92
|
+
break
|
|
93
|
+
current = parent
|
|
94
|
+
|
|
95
|
+
return LintConfig()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _parse_reprompt_toml(path: Path) -> LintConfig:
|
|
99
|
+
"""Parse a .reprompt.toml file."""
|
|
100
|
+
try:
|
|
101
|
+
with open(path, "rb") as f:
|
|
102
|
+
data = tomllib.load(f)
|
|
103
|
+
return _build_config(data.get("lint", {}))
|
|
104
|
+
except Exception:
|
|
105
|
+
return LintConfig()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _parse_pyproject_toml(path: Path) -> LintConfig | None:
|
|
109
|
+
"""Parse [tool.reprompt.lint] from pyproject.toml. Returns None if section missing."""
|
|
110
|
+
try:
|
|
111
|
+
with open(path, "rb") as f:
|
|
112
|
+
data = tomllib.load(f)
|
|
113
|
+
lint_section = data.get("tool", {}).get("reprompt", {}).get("lint")
|
|
114
|
+
if lint_section is None:
|
|
115
|
+
return None
|
|
116
|
+
return _build_config(lint_section)
|
|
117
|
+
except Exception:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _build_config(lint_data: dict) -> LintConfig:
|
|
122
|
+
"""Build LintConfig from a parsed TOML dict."""
|
|
123
|
+
config = LintConfig()
|
|
124
|
+
|
|
125
|
+
# Top-level lint settings
|
|
126
|
+
if "score-threshold" in lint_data:
|
|
127
|
+
config.score_threshold = int(lint_data["score-threshold"])
|
|
128
|
+
|
|
129
|
+
# Rule settings
|
|
130
|
+
rules = lint_data.get("rules", {})
|
|
131
|
+
|
|
132
|
+
# min-length: int threshold or false
|
|
133
|
+
if "min-length" in rules:
|
|
134
|
+
val = rules["min-length"]
|
|
135
|
+
config.min_length = int(val) if val else 0
|
|
136
|
+
|
|
137
|
+
# short-prompt: int threshold or false
|
|
138
|
+
if "short-prompt" in rules:
|
|
139
|
+
val = rules["short-prompt"]
|
|
140
|
+
config.short_prompt = int(val) if val else 0
|
|
141
|
+
|
|
142
|
+
# vague-prompt: bool
|
|
143
|
+
if "vague-prompt" in rules:
|
|
144
|
+
config.vague_prompt = bool(rules["vague-prompt"])
|
|
145
|
+
|
|
146
|
+
# debug-needs-reference: bool
|
|
147
|
+
if "debug-needs-reference" in rules:
|
|
148
|
+
config.debug_needs_reference = bool(rules["debug-needs-reference"])
|
|
149
|
+
|
|
150
|
+
# file-extensions: list
|
|
151
|
+
if "file-extensions" in rules:
|
|
152
|
+
config.file_extensions = list(rules["file-extensions"])
|
|
153
|
+
|
|
154
|
+
return config
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def lint_prompt(text: str, config: LintConfig | None = None) -> list[LintViolation]:
|
|
158
|
+
"""Lint a single prompt against quality rules."""
|
|
159
|
+
if config is None:
|
|
160
|
+
config = DEFAULT_CONFIG
|
|
161
|
+
|
|
162
|
+
violations: list[LintViolation] = []
|
|
163
|
+
stripped = text.strip().lower()
|
|
164
|
+
|
|
165
|
+
# Rule 1: Too short
|
|
166
|
+
if config.min_length > 0 and len(stripped) < config.min_length:
|
|
167
|
+
violations.append(
|
|
168
|
+
LintViolation(
|
|
169
|
+
rule="min-length",
|
|
170
|
+
severity="error",
|
|
171
|
+
message=f"Prompt is too short ({len(stripped)} chars, minimum {config.min_length})",
|
|
172
|
+
prompt_text=text,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
elif config.short_prompt > 0 and len(stripped) < config.short_prompt:
|
|
176
|
+
violations.append(
|
|
177
|
+
LintViolation(
|
|
178
|
+
rule="short-prompt",
|
|
179
|
+
severity="warning",
|
|
180
|
+
message=(f"Prompt is short ({len(stripped)} chars) — consider adding context"),
|
|
181
|
+
prompt_text=text,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Rule 2: Vague starter
|
|
186
|
+
if config.vague_prompt and stripped in VAGUE_STARTERS:
|
|
187
|
+
violations.append(
|
|
188
|
+
LintViolation(
|
|
189
|
+
rule="vague-prompt",
|
|
190
|
+
severity="error",
|
|
191
|
+
message="Prompt is too vague — specify what to fix/change and where",
|
|
192
|
+
prompt_text=text,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Rule 3: No file/function reference in debug prompts
|
|
197
|
+
if config.debug_needs_reference:
|
|
198
|
+
debug_words = {"fix", "debug", "bug", "error", "broken", "failing", "crash"}
|
|
199
|
+
has_debug_intent = any(w in stripped for w in debug_words)
|
|
200
|
+
indicators = list(config.file_extensions) + ["()", "line ", "file "]
|
|
201
|
+
has_reference = any(indicator in text for indicator in indicators)
|
|
202
|
+
min_len = config.min_length if config.min_length > 0 else 20
|
|
203
|
+
if has_debug_intent and not has_reference and len(stripped) >= min_len:
|
|
204
|
+
violations.append(
|
|
205
|
+
LintViolation(
|
|
206
|
+
rule="debug-needs-reference",
|
|
207
|
+
severity="warning",
|
|
208
|
+
message="Debug prompt lacks file/function reference — add specifics",
|
|
209
|
+
prompt_text=text,
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return violations
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def lint_prompts(texts: list[str], config: LintConfig | None = None) -> list[LintViolation]:
|
|
217
|
+
"""Lint multiple prompts and return all violations."""
|
|
218
|
+
violations: list[LintViolation] = []
|
|
219
|
+
for text in texts:
|
|
220
|
+
violations.extend(lint_prompt(text, config))
|
|
221
|
+
return violations
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def format_lint_results(violations: list[LintViolation], total_prompts: int) -> str:
|
|
225
|
+
"""Format lint results for terminal/CI output."""
|
|
226
|
+
if not violations:
|
|
227
|
+
return f"✓ {total_prompts} prompts checked, no issues found"
|
|
228
|
+
|
|
229
|
+
errors = [v for v in violations if v.severity == "error"]
|
|
230
|
+
warnings = [v for v in violations if v.severity == "warning"]
|
|
231
|
+
|
|
232
|
+
lines: list[str] = []
|
|
233
|
+
lines.append(f"Checked {total_prompts} prompts\n")
|
|
234
|
+
|
|
235
|
+
for v in violations:
|
|
236
|
+
prefix = "✗" if v.severity == "error" else "!"
|
|
237
|
+
display = v.prompt_text[:60] + "..." if len(v.prompt_text) > 60 else v.prompt_text
|
|
238
|
+
lines.append(f' {prefix} [{v.rule}] "{display}"')
|
|
239
|
+
lines.append(f" {v.message}")
|
|
240
|
+
|
|
241
|
+
lines.append(f"\n{len(errors)} error(s), {len(warnings)} warning(s)")
|
|
242
|
+
return "\n".join(lines)
|
|
@@ -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()
|
|
@@ -129,25 +129,25 @@ class TestExtensionE2EPipeline:
|
|
|
129
129
|
# Verify ping
|
|
130
130
|
assert responses[0]["type"] == "pong"
|
|
131
131
|
|
|
132
|
-
# Verify sync results
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
132
|
+
# Verify sync results (check core fields; insights key added in v1.9)
|
|
133
|
+
r1 = responses[1]
|
|
134
|
+
assert r1["type"] == "sync_result"
|
|
135
|
+
assert r1["received"] == 2
|
|
136
|
+
assert r1["new_stored"] == 2
|
|
137
|
+
assert r1["duplicates"] == 0
|
|
138
|
+
assert "insights" in r1
|
|
139
|
+
|
|
140
|
+
r2 = responses[2]
|
|
141
|
+
assert r2["type"] == "sync_result"
|
|
142
|
+
assert r2["received"] == 1
|
|
143
|
+
assert r2["new_stored"] == 1
|
|
144
|
+
assert r2["duplicates"] == 0
|
|
145
|
+
|
|
146
|
+
r3 = responses[3]
|
|
147
|
+
assert r3["type"] == "sync_result"
|
|
148
|
+
assert r3["received"] == 1
|
|
149
|
+
assert r3["new_stored"] == 1
|
|
150
|
+
assert r3["duplicates"] == 0
|
|
151
151
|
|
|
152
152
|
# Verify status shows all 4 prompts
|
|
153
153
|
assert responses[4]["type"] == "status"
|