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.
Files changed (300) hide show
  1. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/PKG-INFO +1 -1
  2. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/pyproject.toml +1 -1
  3. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/__init__.py +1 -1
  4. reprompt_cli-1.9.1/src/reprompt/bridge/handler.py +234 -0
  5. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/cli.py +12 -6
  6. reprompt_cli-1.9.1/src/reprompt/core/lint.py +242 -0
  7. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/storage/db.py +17 -0
  8. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_e2e.py +19 -19
  9. reprompt_cli-1.9.1/tests/test_bridge_handler.py +354 -0
  10. reprompt_cli-1.9.1/tests/test_lint.py +237 -0
  11. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/uv.lock +1 -1
  12. reprompt_cli-1.8.1/src/reprompt/bridge/handler.py +0 -100
  13. reprompt_cli-1.8.1/src/reprompt/core/lint.py +0 -112
  14. reprompt_cli-1.8.1/tests/test_bridge_handler.py +0 -118
  15. reprompt_cli-1.8.1/tests/test_lint.py +0 -81
  16. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.editorconfig +0 -0
  17. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  18. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  19. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  20. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  21. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/dependabot.yml +0 -0
  23. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/workflows/ci.yml +0 -0
  24. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.github/workflows/publish.yml +0 -0
  25. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.gitignore +0 -0
  26. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.pre-commit-config.yaml +0 -0
  27. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.pre-commit-hooks.yaml +0 -0
  28. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.testmondata-shm +0 -0
  29. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/.testmondata-wal +0 -0
  30. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/CHANGELOG.md +0 -0
  31. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/CODE_OF_CONDUCT.md +0 -0
  32. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/CONTRIBUTING.md +0 -0
  33. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/LICENSE +0 -0
  34. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/README.md +0 -0
  35. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/SECURITY.md +0 -0
  36. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
  37. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/action.yml +0 -0
  38. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/demo.gif +0 -0
  39. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-128.png +0 -0
  40. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-16.png +0 -0
  41. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-256.png +0 -0
  42. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-32.png +0 -0
  43. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-48.png +0 -0
  44. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-512.png +0 -0
  45. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon-96.png +0 -0
  46. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/brand-icon.svg +0 -0
  47. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-128.png +0 -0
  48. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-16.png +0 -0
  49. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-256.png +0 -0
  50. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-32.png +0 -0
  51. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-48.png +0 -0
  52. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-512.png +0 -0
  53. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-96.png +0 -0
  54. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-favicon.svg +0 -0
  55. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-128.png +0 -0
  56. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-16.png +0 -0
  57. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-256.png +0 -0
  58. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-32.png +0 -0
  59. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-48.png +0 -0
  60. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-512.png +0 -0
  61. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon-96.png +0 -0
  62. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/cli-icon.svg +0 -0
  63. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-128.png +0 -0
  64. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-16.png +0 -0
  65. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-256.png +0 -0
  66. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-32.png +0 -0
  67. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-48.png +0 -0
  68. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-512.png +0 -0
  69. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon-96.png +0 -0
  70. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/favicon.svg +0 -0
  71. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/icons/generate.sh +0 -0
  72. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
  73. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
  74. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/scripts/generate_demo_data.py +0 -0
  75. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/__init__.py +0 -0
  76. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/aider.py +0 -0
  77. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/base.py +0 -0
  78. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/chatgpt.py +0 -0
  79. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/claude_chat.py +0 -0
  80. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/claude_code.py +0 -0
  81. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/cline.py +0 -0
  82. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/codex.py +0 -0
  83. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/cursor.py +0 -0
  84. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/filters.py +0 -0
  85. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/gemini.py +0 -0
  86. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/adapters/openclaw.py +0 -0
  87. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/bridge/__init__.py +0 -0
  88. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/bridge/host.py +0 -0
  89. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/bridge/manifest.py +0 -0
  90. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/bridge/protocol.py +0 -0
  91. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/commands/__init__.py +0 -0
  92. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/commands/telemetry.py +0 -0
  93. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/commands/wrapped.py +0 -0
  94. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/config.py +0 -0
  95. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/__init__.py +0 -0
  96. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/agent.py +0 -0
  97. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/analyzer.py +0 -0
  98. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/compress.py +0 -0
  99. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/conversation.py +0 -0
  100. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/cost.py +0 -0
  101. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/dashboard.py +0 -0
  102. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/dedup.py +0 -0
  103. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/digest.py +0 -0
  104. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/distill.py +0 -0
  105. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/effectiveness.py +0 -0
  106. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/extractors.py +0 -0
  107. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/extractors_zh.py +0 -0
  108. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/insights.py +0 -0
  109. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/lang_detect.py +0 -0
  110. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/library.py +0 -0
  111. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/merge_view.py +0 -0
  112. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/models.py +0 -0
  113. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/persona.py +0 -0
  114. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/pipeline.py +0 -0
  115. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/privacy.py +0 -0
  116. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/privacy_scan.py +0 -0
  117. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/prompt_dna.py +0 -0
  118. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/recommend.py +0 -0
  119. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/repetition.py +0 -0
  120. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/scorer.py +0 -0
  121. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/segmenter.py +0 -0
  122. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/session_meta.py +0 -0
  123. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/session_quality.py +0 -0
  124. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/session_type.py +0 -0
  125. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/style.py +0 -0
  126. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/suggestions.py +0 -0
  127. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/templates.py +0 -0
  128. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/timeutil.py +0 -0
  129. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/trends.py +0 -0
  130. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/core/wrapped.py +0 -0
  131. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/demo.py +0 -0
  132. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/__init__.py +0 -0
  133. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/base.py +0 -0
  134. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/local_embed.py +0 -0
  135. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/ollama.py +0 -0
  136. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/openai_embed.py +0 -0
  137. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/embeddings/tfidf.py +0 -0
  138. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/mcp.py +0 -0
  139. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/mcp_main.py +0 -0
  140. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/__init__.py +0 -0
  141. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/agent_terminal.py +0 -0
  142. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/chartjs.min.js +0 -0
  143. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/compress_terminal.py +0 -0
  144. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/dashboard_terminal.py +0 -0
  145. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/distill_terminal.py +0 -0
  146. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/export.py +0 -0
  147. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/html_report.py +0 -0
  148. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/json_out.py +0 -0
  149. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/markdown.py +0 -0
  150. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/repetition_terminal.py +0 -0
  151. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/sessions_terminal.py +0 -0
  152. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/terminal.py +0 -0
  153. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/wrapped_html.py +0 -0
  154. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/output/wrapped_terminal.py +0 -0
  155. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/py.typed +0 -0
  156. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/sharing/__init__.py +0 -0
  157. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/sharing/client.py +0 -0
  158. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/sharing/clipboard.py +0 -0
  159. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/storage/__init__.py +0 -0
  160. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/__init__.py +0 -0
  161. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/collector.py +0 -0
  162. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/consent.py +0 -0
  163. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/events.py +0 -0
  164. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/prompt.py +0 -0
  165. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/queue.py +0 -0
  166. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/src/reprompt/telemetry/sender.py +0 -0
  167. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/__init__.py +0 -0
  168. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/conftest.py +0 -0
  169. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/aider_chat_history.md +0 -0
  170. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/chatgpt_conversations.json +0 -0
  171. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/claude_chat_export.json +0 -0
  172. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/claude_session.jsonl +0 -0
  173. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
  174. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/export/default_export.md +0 -0
  175. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/export/full_export.md +0 -0
  176. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/gemini_session.json +0 -0
  177. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/fixtures/openclaw_session.jsonl +0 -0
  178. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_aider.py +0 -0
  179. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_chatgpt.py +0 -0
  180. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_claude.py +0 -0
  181. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_claude_chat.py +0 -0
  182. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_cline.py +0 -0
  183. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_gemini.py +0 -0
  184. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_adapter_openclaw.py +0 -0
  185. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_agent.py +0 -0
  186. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_agent_cli.py +0 -0
  187. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_analyzer.py +0 -0
  188. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_cli.py +0 -0
  189. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_integration.py +0 -0
  190. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_manifest.py +0 -0
  191. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_bridge_protocol.py +0 -0
  192. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cli.py +0 -0
  193. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cli_deprecations.py +0 -0
  194. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cli_library_effectiveness.py +0 -0
  195. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_clipboard.py +0 -0
  196. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_codex_adapter.py +0 -0
  197. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compare_best_worst.py +0 -0
  198. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress.py +0 -0
  199. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress_cli.py +0 -0
  200. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress_dna.py +0 -0
  201. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress_html.py +0 -0
  202. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_compress_insights.py +0 -0
  203. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_config.py +0 -0
  204. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_conversation.py +0 -0
  205. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_copy_flag.py +0 -0
  206. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cost.py +0 -0
  207. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_coverage_boost.py +0 -0
  208. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_cursor_adapter.py +0 -0
  209. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_dashboard.py +0 -0
  210. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db.py +0 -0
  211. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db_digest.py +0 -0
  212. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db_effectiveness.py +0 -0
  213. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db_session_quality.py +0 -0
  214. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_db_trends.py +0 -0
  215. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_dedup.py +0 -0
  216. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_demo.py +0 -0
  217. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_deprecated_commands.py +0 -0
  218. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_digest.py +0 -0
  219. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_digest_cli.py +0 -0
  220. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_distill.py +0 -0
  221. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_distill_cli.py +0 -0
  222. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_distill_weights.py +0 -0
  223. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_e2e.py +0 -0
  224. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_effectiveness.py +0 -0
  225. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_embeddings_local.py +0 -0
  226. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_embeddings_ollama.py +0 -0
  227. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_embeddings_openai.py +0 -0
  228. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_empty_state.py +0 -0
  229. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_export.py +0 -0
  230. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_export_cli.py +0 -0
  231. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_export_snapshot.py +0 -0
  232. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_extractors.py +0 -0
  233. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_extractors_routing.py +0 -0
  234. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_extractors_zh.py +0 -0
  235. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_extractors_zh_e2e.py +0 -0
  236. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_html_report.py +0 -0
  237. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_import_cli.py +0 -0
  238. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_import_e2e.py +0 -0
  239. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_insights.py +0 -0
  240. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_insights_cli.py +0 -0
  241. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_insights_expanded.py +0 -0
  242. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_install_hook.py +0 -0
  243. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_lang_detect.py +0 -0
  244. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_library.py +0 -0
  245. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_lint_cli.py +0 -0
  246. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_markdown.py +0 -0
  247. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_mcp.py +0 -0
  248. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_merge_view.py +0 -0
  249. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_models.py +0 -0
  250. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_output.py +0 -0
  251. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_parse_conversation_base.py +0 -0
  252. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_parse_conversation_chatgpt.py +0 -0
  253. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_parse_conversation_claude.py +0 -0
  254. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_persona.py +0 -0
  255. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_pipeline.py +0 -0
  256. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy.py +0 -0
  257. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy_cli.py +0 -0
  258. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy_e2e.py +0 -0
  259. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy_output.py +0 -0
  260. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_privacy_scan.py +0 -0
  261. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_prompt_dna.py +0 -0
  262. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_public_api.py +0 -0
  263. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_recommend.py +0 -0
  264. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_repetition.py +0 -0
  265. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_repetition_cli.py +0 -0
  266. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_repetition_output.py +0 -0
  267. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_schema_version.py +0 -0
  268. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_score_cli.py +0 -0
  269. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_scorer.py +0 -0
  270. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_segmenter.py +0 -0
  271. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_session_quality.py +0 -0
  272. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_session_type.py +0 -0
  273. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_sessions_cli.py +0 -0
  274. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_sessions_output.py +0 -0
  275. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_share_e2e.py +0 -0
  276. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_sharing_client.py +0 -0
  277. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_source_filter.py +0 -0
  278. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_style.py +0 -0
  279. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_style_trends.py +0 -0
  280. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_suggestions.py +0 -0
  281. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_cli.py +0 -0
  282. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_collector.py +0 -0
  283. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_consent.py +0 -0
  284. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_e2e.py +0 -0
  285. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_events.py +0 -0
  286. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_prompt.py +0 -0
  287. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_queue.py +0 -0
  288. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_telemetry_sender.py +0 -0
  289. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_template_cli.py +0 -0
  290. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_templates.py +0 -0
  291. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_timeutil.py +0 -0
  292. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_trends.py +0 -0
  293. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_trends_cli.py +0 -0
  294. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_use_cli.py +0 -0
  295. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped.py +0 -0
  296. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped_cli.py +0 -0
  297. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped_e2e.py +0 -0
  298. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped_html.py +0 -0
  299. {reprompt_cli-1.8.1 → reprompt_cli-1.9.1}/tests/test_wrapped_output.py +0 -0
  300. {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.8.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reprompt-cli"
3
- version = "1.8.1"
3
+ version = "1.9.1"
4
4
  description = "Discover, analyze, and optimize your prompts from AI coding sessions"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -1,6 +1,6 @@
1
1
  """reprompt — Discover, analyze, and evolve your best prompts from AI coding sessions."""
2
2
 
3
- __version__ = "1.8.1"
3
+ __version__ = "1.9.1"
4
4
 
5
5
  __all__ = [
6
6
  "__version__",
@@ -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 score_threshold > 0:
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": score_threshold,
879
- "pass": avg_score >= score_threshold,
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 {score_threshold} → {status}"
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
- assert responses[1] == {
134
- "type": "sync_result",
135
- "received": 2,
136
- "new_stored": 2,
137
- "duplicates": 0,
138
- }
139
- assert responses[2] == {
140
- "type": "sync_result",
141
- "received": 1,
142
- "new_stored": 1,
143
- "duplicates": 0,
144
- }
145
- assert responses[3] == {
146
- "type": "sync_result",
147
- "received": 1,
148
- "new_stored": 1,
149
- "duplicates": 0,
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"