reprompt-cli 1.4.0__tar.gz → 1.4.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 (242) hide show
  1. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/PKG-INFO +1 -1
  2. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/docs/roadmap.md +8 -6
  3. reprompt_cli-1.4.1/docs/superpowers/specs/2026-03-24-v141-polish-design.md +184 -0
  4. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/pyproject.toml +1 -1
  5. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/__init__.py +1 -1
  6. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/cli.py +67 -6
  7. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/style.py +62 -1
  8. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/terminal.py +90 -0
  9. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/storage/db.py +40 -0
  10. reprompt_cli-1.4.1/tests/test_compare_best_worst.py +233 -0
  11. reprompt_cli-1.4.1/tests/test_style_trends.py +256 -0
  12. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.editorconfig +0 -0
  13. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  15. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  17. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.github/dependabot.yml +0 -0
  19. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.github/workflows/ci.yml +0 -0
  20. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.github/workflows/publish.yml +0 -0
  21. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.gitignore +0 -0
  22. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.pre-commit-config.yaml +0 -0
  23. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/.testmondata +0 -0
  24. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/CHANGELOG.md +0 -0
  25. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/CLAUDE.md +0 -0
  26. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/CODE_OF_CONDUCT.md +0 -0
  27. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/CONTRIBUTING.md +0 -0
  28. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/LICENSE +0 -0
  29. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/README.md +0 -0
  30. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/SECURITY.md +0 -0
  31. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/action.yml +0 -0
  32. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/docs/launch-post.md +0 -0
  33. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/docs/superpowers/specs/2026-03-11-html-dashboard-design.md +0 -0
  34. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/docs/superpowers/specs/2026-03-11-merge-view-design.md +0 -0
  35. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/docs/superpowers/specs/2026-03-11-prompt-templates-design.md +0 -0
  36. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/docs/superpowers/specs/2026-03-22-prompt-compress-design.md +0 -0
  37. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/docs/superpowers/specs/2026-03-23-distill-design.md +0 -0
  38. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/docs/superpowers/specs/2026-03-23-v131-suggestions-source-design.md +0 -0
  39. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/docs/superpowers/specs/2026-03-24-v14-context-recovery-design.md +0 -0
  40. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/module.yaml +0 -0
  41. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/scripts/generate_demo_data.py +0 -0
  42. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/scripts/launch/hn_monitor.py +0 -0
  43. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/scripts/launch/reddit_helper.py +0 -0
  44. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/__init__.py +0 -0
  45. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/aider.py +0 -0
  46. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/base.py +0 -0
  47. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/chatgpt.py +0 -0
  48. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/claude_chat.py +0 -0
  49. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/claude_code.py +0 -0
  50. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/cline.py +0 -0
  51. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/cursor.py +0 -0
  52. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/filters.py +0 -0
  53. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/gemini.py +0 -0
  54. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/adapters/openclaw.py +0 -0
  55. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/bridge/__init__.py +0 -0
  56. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/bridge/handler.py +0 -0
  57. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/bridge/host.py +0 -0
  58. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/bridge/manifest.py +0 -0
  59. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/bridge/protocol.py +0 -0
  60. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/commands/__init__.py +0 -0
  61. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/commands/telemetry.py +0 -0
  62. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/commands/wrapped.py +0 -0
  63. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/config.py +0 -0
  64. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/__init__.py +0 -0
  65. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/analyzer.py +0 -0
  66. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/compress.py +0 -0
  67. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/conversation.py +0 -0
  68. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/dedup.py +0 -0
  69. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/digest.py +0 -0
  70. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/distill.py +0 -0
  71. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/effectiveness.py +0 -0
  72. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/extractors.py +0 -0
  73. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/extractors_zh.py +0 -0
  74. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/insights.py +0 -0
  75. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/lang_detect.py +0 -0
  76. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/library.py +0 -0
  77. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/lint.py +0 -0
  78. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/merge_view.py +0 -0
  79. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/models.py +0 -0
  80. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/persona.py +0 -0
  81. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/pipeline.py +0 -0
  82. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/privacy.py +0 -0
  83. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/prompt_dna.py +0 -0
  84. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/recommend.py +0 -0
  85. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/scorer.py +0 -0
  86. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/segmenter.py +0 -0
  87. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/session_meta.py +0 -0
  88. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/suggestions.py +0 -0
  89. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/templates.py +0 -0
  90. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/timeutil.py +0 -0
  91. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/trends.py +0 -0
  92. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/core/wrapped.py +0 -0
  93. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/demo.py +0 -0
  94. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/embeddings/__init__.py +0 -0
  95. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/embeddings/base.py +0 -0
  96. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/embeddings/local_embed.py +0 -0
  97. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/embeddings/ollama.py +0 -0
  98. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/embeddings/openai_embed.py +0 -0
  99. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/embeddings/tfidf.py +0 -0
  100. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/mcp.py +0 -0
  101. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/mcp_main.py +0 -0
  102. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/__init__.py +0 -0
  103. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/chartjs.min.js +0 -0
  104. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/compress_terminal.py +0 -0
  105. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/distill_terminal.py +0 -0
  106. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/export.py +0 -0
  107. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/html_report.py +0 -0
  108. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/json_out.py +0 -0
  109. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/markdown.py +0 -0
  110. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/wrapped_html.py +0 -0
  111. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/output/wrapped_terminal.py +0 -0
  112. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/py.typed +0 -0
  113. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/sharing/__init__.py +0 -0
  114. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/sharing/client.py +0 -0
  115. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/sharing/clipboard.py +0 -0
  116. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/storage/__init__.py +0 -0
  117. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/telemetry/__init__.py +0 -0
  118. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/telemetry/collector.py +0 -0
  119. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/telemetry/consent.py +0 -0
  120. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/telemetry/events.py +0 -0
  121. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/telemetry/prompt.py +0 -0
  122. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/telemetry/queue.py +0 -0
  123. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/src/reprompt/telemetry/sender.py +0 -0
  124. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/__init__.py +0 -0
  125. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/conftest.py +0 -0
  126. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/fixtures/aider_chat_history.md +0 -0
  127. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/fixtures/chatgpt_conversations.json +0 -0
  128. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/fixtures/claude_chat_export.json +0 -0
  129. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/fixtures/claude_session.jsonl +0 -0
  130. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
  131. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/fixtures/export/default_export.md +0 -0
  132. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/fixtures/export/full_export.md +0 -0
  133. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/fixtures/gemini_session.json +0 -0
  134. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/fixtures/openclaw_session.jsonl +0 -0
  135. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_adapter_aider.py +0 -0
  136. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_adapter_chatgpt.py +0 -0
  137. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_adapter_claude.py +0 -0
  138. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_adapter_claude_chat.py +0 -0
  139. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_adapter_cline.py +0 -0
  140. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_adapter_gemini.py +0 -0
  141. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_adapter_openclaw.py +0 -0
  142. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_analyzer.py +0 -0
  143. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_bridge_cli.py +0 -0
  144. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_bridge_handler.py +0 -0
  145. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_bridge_integration.py +0 -0
  146. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_bridge_manifest.py +0 -0
  147. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_bridge_protocol.py +0 -0
  148. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_cli.py +0 -0
  149. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_cli_library_effectiveness.py +0 -0
  150. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_clipboard.py +0 -0
  151. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_compress.py +0 -0
  152. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_compress_cli.py +0 -0
  153. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_compress_dna.py +0 -0
  154. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_compress_html.py +0 -0
  155. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_compress_insights.py +0 -0
  156. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_config.py +0 -0
  157. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_conversation.py +0 -0
  158. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_coverage_boost.py +0 -0
  159. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_cursor_adapter.py +0 -0
  160. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_db.py +0 -0
  161. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_db_digest.py +0 -0
  162. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_db_effectiveness.py +0 -0
  163. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_db_trends.py +0 -0
  164. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_dedup.py +0 -0
  165. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_demo.py +0 -0
  166. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_deprecated_commands.py +0 -0
  167. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_digest.py +0 -0
  168. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_digest_cli.py +0 -0
  169. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_distill.py +0 -0
  170. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_distill_cli.py +0 -0
  171. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_distill_weights.py +0 -0
  172. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_e2e.py +0 -0
  173. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_effectiveness.py +0 -0
  174. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_embeddings_local.py +0 -0
  175. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_embeddings_ollama.py +0 -0
  176. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_embeddings_openai.py +0 -0
  177. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_empty_state.py +0 -0
  178. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_export.py +0 -0
  179. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_export_cli.py +0 -0
  180. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_export_snapshot.py +0 -0
  181. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_extractors.py +0 -0
  182. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_extractors_routing.py +0 -0
  183. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_extractors_zh.py +0 -0
  184. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_extractors_zh_e2e.py +0 -0
  185. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_html_report.py +0 -0
  186. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_import_cli.py +0 -0
  187. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_import_e2e.py +0 -0
  188. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_insights.py +0 -0
  189. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_insights_cli.py +0 -0
  190. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_insights_expanded.py +0 -0
  191. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_install_hook.py +0 -0
  192. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_lang_detect.py +0 -0
  193. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_library.py +0 -0
  194. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_lint.py +0 -0
  195. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_lint_cli.py +0 -0
  196. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_markdown.py +0 -0
  197. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_mcp.py +0 -0
  198. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_merge_view.py +0 -0
  199. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_models.py +0 -0
  200. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_output.py +0 -0
  201. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_parse_conversation_base.py +0 -0
  202. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_parse_conversation_chatgpt.py +0 -0
  203. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_parse_conversation_claude.py +0 -0
  204. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_persona.py +0 -0
  205. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_pipeline.py +0 -0
  206. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_privacy.py +0 -0
  207. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_privacy_cli.py +0 -0
  208. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_privacy_e2e.py +0 -0
  209. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_privacy_output.py +0 -0
  210. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_prompt_dna.py +0 -0
  211. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_public_api.py +0 -0
  212. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_recommend.py +0 -0
  213. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_schema_version.py +0 -0
  214. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_score_cli.py +0 -0
  215. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_scorer.py +0 -0
  216. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_segmenter.py +0 -0
  217. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_share_e2e.py +0 -0
  218. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_sharing_client.py +0 -0
  219. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_source_filter.py +0 -0
  220. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_style.py +0 -0
  221. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_suggestions.py +0 -0
  222. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_telemetry_cli.py +0 -0
  223. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_telemetry_collector.py +0 -0
  224. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_telemetry_consent.py +0 -0
  225. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_telemetry_e2e.py +0 -0
  226. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_telemetry_events.py +0 -0
  227. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_telemetry_prompt.py +0 -0
  228. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_telemetry_queue.py +0 -0
  229. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_telemetry_sender.py +0 -0
  230. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_template_cli.py +0 -0
  231. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_templates.py +0 -0
  232. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_timeutil.py +0 -0
  233. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_trends.py +0 -0
  234. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_trends_cli.py +0 -0
  235. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_use_cli.py +0 -0
  236. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_wrapped.py +0 -0
  237. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_wrapped_cli.py +0 -0
  238. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_wrapped_e2e.py +0 -0
  239. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_wrapped_html.py +0 -0
  240. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_wrapped_output.py +0 -0
  241. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/tests/test_wrapped_share.py +0 -0
  242. {reprompt_cli-1.4.0 → reprompt_cli-1.4.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reprompt-cli
3
- Version: 1.4.0
3
+ Version: 1.4.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
  # reprompt Roadmap
2
2
 
3
- > Last updated: 2026-03-24 · Current version: v1.4.0
3
+ > Last updated: 2026-03-25 · Current version: v1.4.1
4
4
 
5
5
  ## Vision
6
6
 
@@ -35,9 +35,10 @@ Claude Code · OpenClaw · Cursor IDE · Aider · Gemini CLI · Cline · ChatGPT
35
35
  | v1.3.0 | Conversation distillation | `reprompt distill` — 6-signal importance scoring for conversation turns |
36
36
  | v1.3.1 | UX polish | Actionable suggestions on 5 commands, `--source` filter on all data commands |
37
37
  | v1.4.0 | Context recovery + consolidation | `distill --export` context document, signal transparency, command consolidation (27→23) |
38
+ | v1.4.1 | Compare + style polish | `compare --best-worst` auto-pick, `style --trends` period-over-period deltas |
38
39
 
39
40
  ### Quality
40
- - 1295 tests, ≥90% coverage
41
+ - 1316 tests, ≥90% coverage
41
42
  - Strict mypy, ruff lint/format
42
43
  - CI: coverage gate + pre-publish test step
43
44
  - Stable public API (`score_prompt`, `compare_prompts`, `extract_features`)
@@ -51,12 +52,12 @@ Claude Code · OpenClaw · Cursor IDE · Aider · Gemini CLI · Cline · ChatGPT
51
52
  | P1 | `distill --export` context recovery | **DONE** — community signal: resume sessions after compaction/timeout |
52
53
  | P2 | Command consolidation: `save`/`templates`/`use` → `template [save\|list\|use]` | **DONE** — 3 commands doing 1 thing = cognitive overload |
53
54
  | P2 | Command consolidation: `effectiveness`/`merge-view` → `insights` sub-insights | **DONE** — concepts unclear to users |
54
- | P3 | `style` shows change trends | "specificity +12% this week" drives revisits |
55
+ | P3 | `style` shows change trends | **DONE** `--trends` flag with period-over-period deltas |
55
56
  | P4 | `distill --show-weights` / `--weights` signal transparency | Community request for weight visibility |
56
- | P5 | `compare --best-worst` auto-pick | Auto-pick best/worst from DB |
57
- | P5 | `--copy` as standard option on remaining commands | Consistency |
57
+ | P5 | `compare --best-worst` auto-pick | **DONE** — auto-selects from DB scores |
58
+ | P5 | `--copy` as standard option on remaining commands | Decided against — no clear paste destination for analysis commands |
58
59
 
59
- **Status: 27 → 23 visible commands. P1+P2 shipped. Context recovery + consolidation done.**
60
+ **Status: 27 → 23 visible commands. P1+P2+P3+P5 shipped.**
60
61
 
61
62
  ---
62
63
 
@@ -64,6 +65,7 @@ Claude Code · OpenClaw · Cursor IDE · Aider · Gemini CLI · Cline · ChatGPT
64
65
 
65
66
  | Feature | Description |
66
67
  |---------|-------------|
68
+ | Distill false positive reduction | Position signal breaks on small-talk openers / "thanks bye" closers; long error dumps score high on length+uniqueness but aren't decision points; "ok try again" triggers error_recovery but is noise. Community feedback from r/LLMDevs. |
67
69
  | Sensitive content detection | Privacy narrative; PII in prompts |
68
70
  | Agent workflow analysis | Multi-step agent session patterns |
69
71
  | `.reprompt.yml` configurable lint | Team/Pro direction |
@@ -0,0 +1,184 @@
1
+ # v1.4.1 Polish Design Spec
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Two UX improvements that make existing commands more useful without adding new commands.
6
+
7
+ **Version:** v1.4.1 (patch release)
8
+
9
+ ---
10
+
11
+ ## Feature 1: `compare --best-worst`
12
+
13
+ ### Problem
14
+
15
+ `reprompt compare` requires users to manually type two prompt strings. Most users want a quick "show me my best vs worst" without digging through the database.
16
+
17
+ ### Solution
18
+
19
+ Add `--best-worst` flag that auto-selects the highest-scored and lowest-scored prompts from `prompt_features`, then runs the existing comparison logic.
20
+
21
+ ### Interface
22
+
23
+ ```bash
24
+ # Existing (unchanged)
25
+ reprompt compare "prompt A" "prompt B"
26
+
27
+ # New
28
+ reprompt compare --best-worst
29
+ reprompt compare --best-worst --json
30
+ reprompt compare --best-worst --source claude-code
31
+ ```
32
+
33
+ ### Implementation Details
34
+
35
+ **CLI changes (`cli.py`):**
36
+ - `prompt_a` and `prompt_b` become `Optional[str]` (default `None`)
37
+ - Add `--best-worst` flag (bool, default `False`)
38
+ - Add `--source` filter (str, optional) for consistency
39
+ - Manual mutual-exclusion guard at top of function body (Typer has no built-in mechanism):
40
+ ```python
41
+ if best_worst and (prompt_a or prompt_b):
42
+ console.print("[red]--best-worst cannot be combined with prompt arguments[/red]")
43
+ raise typer.Exit(1)
44
+ if not best_worst and not (prompt_a and prompt_b):
45
+ console.print("[red]Provide two prompts or use --best-worst[/red]")
46
+ raise typer.Exit(1)
47
+ ```
48
+ Pattern matches existing `distill` command (cli.py:963–971).
49
+ - When `--best-worst`: call `db.get_best_worst_prompts(source)` to get texts, then run existing scoring logic
50
+
51
+ **DB method (`storage/db.py`):**
52
+ ```python
53
+ def get_best_worst_prompts(self, source: str | None = None) -> tuple[str, str] | None:
54
+ """Return (best_text, worst_text) from scored prompts.
55
+
56
+ Filters to prompts with word_count >= 5 to avoid noise.
57
+ Returns None if fewer than 2 qualifying prompts exist.
58
+ """
59
+ ```
60
+
61
+ Query: JOIN `prompt_features` ON `prompts.hash = prompt_features.prompt_hash`, filter `overall_score IS NOT NULL`, order by score DESC for best and ASC for worst. Filter in Python after the JOIN: fetch all scored rows with their `prompts.text`, apply `len(text.split()) >= 5`, then take max/min `overall_score` rows from survivors. This avoids JSON parsing in the hot path and is straightforward to test.
62
+
63
+ **Terminal output enhancement:**
64
+ When using `--best-worst`, show the prompt texts (truncated to 80 chars) above the comparison table so the user knows what's being compared.
65
+
66
+ ### Edge Cases
67
+
68
+ - Fewer than 2 scored prompts: print guidance ("Run `reprompt scan` then `reprompt score` to build your score history")
69
+ - Best and worst are the same prompt (only 1 unique score): print message ("All prompts have similar scores")
70
+ - `--best-worst` combined with positional args: error with clear message
71
+
72
+ ---
73
+
74
+ ## Feature 2: `style --trends`
75
+
76
+ ### Problem
77
+
78
+ `reprompt style` shows a static snapshot. Users have no way to see if their prompting style is improving over time.
79
+
80
+ ### Solution
81
+
82
+ Add `--trends` flag that compares current period vs previous period style metrics, showing deltas.
83
+
84
+ ### Interface
85
+
86
+ ```bash
87
+ # Existing (unchanged)
88
+ reprompt style
89
+ reprompt style --json
90
+
91
+ # New
92
+ reprompt style --trends
93
+ reprompt style --trends --period 30d
94
+ reprompt style --trends --json
95
+ reprompt style --trends --source claude-code
96
+ ```
97
+
98
+ ### Implementation Details
99
+
100
+ **New function in `core/style.py`:**
101
+ ```python
102
+ def compute_style_trends(
103
+ db: PromptDB,
104
+ period: str = "7d",
105
+ source: str | None = None,
106
+ ) -> dict[str, Any]:
107
+ """Compare style between current and previous period.
108
+
109
+ Returns dict with:
110
+ period, current (style dict), previous (style dict),
111
+ deltas: {specificity, avg_length, top_category_changed, prompt_count}
112
+ """
113
+ ```
114
+
115
+ Reuses:
116
+ - `sliding_windows(period, count=2)` from `core/timeutil` — returns `list[TimeWindow]` with `.start`/`.end` as `datetime` objects
117
+ - `db.get_prompts_in_range(start, end, source=source)` — `source` is keyword-only; `start`/`end` must be ISO-8601 strings, so call `window.start.isoformat()` / `window.end.isoformat()` before passing
118
+ - `compute_style(prompts)` applied to each window's prompts
119
+ - `categorize_prompt()` from `core/library` to build prompt dicts
120
+
121
+ **Delta computation:**
122
+ - `specificity_delta`: current - previous (e.g., +0.12)
123
+ - `avg_length_delta`: current - previous (e.g., +15.3 chars)
124
+ - `prompt_count_delta`: current - previous
125
+ - `top_category_changed`: bool (did the top category shift?)
126
+ - `top_category_current` / `top_category_previous`: for display
127
+
128
+ **CLI changes (`cli.py`):**
129
+ - Add `--trends` flag (bool, default `False`)
130
+ - Add `--period` option (str, default `"7d"`)
131
+ - When `--trends`: call `compute_style_trends()`, render with new function
132
+
133
+ **Terminal rendering (`output/terminal.py`):**
134
+ New `render_style_trends(data)` function. Output format:
135
+
136
+ ```
137
+ Style Trends (7d)
138
+ ─────────────────────────────────
139
+ Specificity 0.72 → 0.84 (+12%)
140
+ Avg Length 45 → 52 chars (+16%)
141
+ Prompts 23 → 31 (+35%)
142
+ Top Category debugging → refactoring
143
+ ```
144
+
145
+ Coloring rules:
146
+ - `Specificity`: green if positive delta (higher = better), red if negative
147
+ - `Prompts`: green if positive delta (more activity = good), dim if negative
148
+ - `Avg Length`: always `[dim]` (neutral metric, no green/red — length is not inherently better or worse)
149
+ - `Top Category`: no color, just show the shift
150
+
151
+ **JSON output:** Return raw `{period, current, previous, deltas}` dict.
152
+
153
+ ### Edge Cases
154
+
155
+ - No prompts in either window: "Not enough data for trends. Keep prompting and check back next week."
156
+ - No prompts in previous window only: show current stats with "New! No previous data to compare."
157
+ - `--trends` combined with existing `--json`: works (returns trends JSON instead of snapshot JSON)
158
+
159
+ ---
160
+
161
+ ## What's NOT in v1.4.1
162
+
163
+ - `--copy` standardization: decided against (no clear paste destination for analysis commands)
164
+ - New commands: none
165
+ - Breaking changes: none
166
+
167
+ ## Testing
168
+
169
+ - `compare --best-worst`: ~8 tests (DB method, CLI integration, edge cases, JSON output, source filter, mutual exclusion with args)
170
+ - `style --trends`: ~8 tests (compute function, CLI integration, edge cases, JSON output, source filter, period option)
171
+ - Total estimated: ~16 new tests
172
+
173
+ ## Files Modified
174
+
175
+ | File | Change |
176
+ |------|--------|
177
+ | `src/reprompt/cli.py` | `compare` gains `--best-worst`/`--source`; `style` gains `--trends`/`--period` |
178
+ | `src/reprompt/storage/db.py` | Add `get_best_worst_prompts()` |
179
+ | `src/reprompt/core/style.py` | Add `compute_style_trends()` |
180
+ | `src/reprompt/output/terminal.py` | Add `render_style_trends()`, modify `render_compare()` for prompt text display |
181
+ | `pyproject.toml` | Version bump to 1.4.1 |
182
+ | `src/reprompt/__init__.py` | Version bump to 1.4.1 |
183
+ | `tests/test_compare_best_worst.py` | New test file |
184
+ | `tests/test_style_trends.py` | New test file |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reprompt-cli"
3
- version = "1.4.0"
3
+ version = "1.4.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.4.0"
3
+ __version__ = "1.4.1"
4
4
 
5
5
  __all__ = [
6
6
  "__version__",
@@ -709,18 +709,34 @@ def style(
709
709
  source: str | None = typer.Option(
710
710
  None, "--source", "-s", help="Filter by source (e.g. claude-code, cursor, aider)"
711
711
  ),
712
+ trends: bool = typer.Option(False, "--trends", help="Show style change trends"),
713
+ period: str = typer.Option("7d", "--period", help="Comparison period (with --trends)"),
712
714
  ) -> None:
713
715
  """Show your personal prompting style fingerprint."""
714
716
  import json as json_mod
715
717
 
716
718
  from reprompt.config import Settings
717
- from reprompt.core.library import categorize_prompt
718
- from reprompt.core.style import compute_style
719
- from reprompt.output.terminal import render_style
720
719
  from reprompt.storage.db import PromptDB
721
720
 
722
721
  settings = Settings()
723
722
  db = PromptDB(settings.db_path)
723
+
724
+ if trends:
725
+ from reprompt.core.style import compute_style_trends
726
+ from reprompt.output.terminal import render_style_trends
727
+
728
+ data = compute_style_trends(db, period=period, source=source)
729
+
730
+ if json_output:
731
+ print(json_mod.dumps(data, indent=2))
732
+ else:
733
+ print(render_style_trends(data), end="")
734
+ return
735
+
736
+ from reprompt.core.library import categorize_prompt
737
+ from reprompt.core.style import compute_style
738
+ from reprompt.output.terminal import render_style
739
+
724
740
  rows = db.get_all_prompts(source=source)
725
741
  prompts = [
726
742
  {
@@ -1231,15 +1247,55 @@ def _load_conversation(
1231
1247
 
1232
1248
  @app.command()
1233
1249
  def compare(
1234
- prompt_a: str = typer.Argument(..., help="First prompt"),
1235
- prompt_b: str = typer.Argument(..., help="Second prompt"),
1250
+ prompt_a: str | None = typer.Argument(None, help="First prompt"),
1251
+ prompt_b: str | None = typer.Argument(None, help="Second prompt"),
1236
1252
  json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
1253
+ best_worst: bool = typer.Option(
1254
+ False, "--best-worst", help="Auto-select best and worst from DB"
1255
+ ),
1256
+ source: str | None = typer.Option(
1257
+ None, "--source", "-s", help="Filter by source (with --best-worst)"
1258
+ ),
1237
1259
  ) -> None:
1238
1260
  """Compare two prompts side by side using Prompt DNA analysis."""
1261
+ from typing import Any
1262
+
1239
1263
  from reprompt.core.extractors import extract_features
1240
1264
  from reprompt.core.prompt_dna import PromptDNA
1241
1265
  from reprompt.core.scorer import ScoreBreakdown, score_prompt
1242
1266
 
1267
+ # Mutual exclusion guard
1268
+ if best_worst and (prompt_a or prompt_b):
1269
+ console.print("[red]--best-worst cannot be combined with prompt arguments[/red]")
1270
+ raise typer.Exit(1)
1271
+ if not best_worst and not (prompt_a and prompt_b):
1272
+ console.print("[red]Provide two prompts or use --best-worst[/red]")
1273
+ raise typer.Exit(1)
1274
+
1275
+ prompt_a_text: str | None = None
1276
+ prompt_b_text: str | None = None
1277
+
1278
+ if best_worst:
1279
+ from reprompt.config import Settings
1280
+ from reprompt.storage.db import PromptDB
1281
+
1282
+ settings = Settings()
1283
+ db = PromptDB(settings.db_path)
1284
+ pair = db.get_best_worst_prompts(source=source)
1285
+ if pair is None:
1286
+ console.print(
1287
+ "Not enough scored prompts. Run [bold]reprompt scan[/bold]"
1288
+ " to build your score history."
1289
+ )
1290
+ raise typer.Exit(1)
1291
+ prompt_a = pair[0] # best
1292
+ prompt_b = pair[1] # worst
1293
+ prompt_a_text = prompt_a
1294
+ prompt_b_text = prompt_b
1295
+
1296
+ # Type narrowing for mypy strict (guards above guarantee non-None)
1297
+ assert prompt_a is not None and prompt_b is not None
1298
+
1243
1299
  dna_a = extract_features(prompt_a, source="manual", session_id="compare-a")
1244
1300
  dna_b = extract_features(prompt_b, source="manual", session_id="compare-b")
1245
1301
  score_a = score_prompt(dna_a)
@@ -1259,11 +1315,16 @@ def compare(
1259
1315
  "ambiguity_score": dna.ambiguity_score,
1260
1316
  }
1261
1317
 
1262
- result = {
1318
+ result: dict[str, Any] = {
1263
1319
  "prompt_a": _build_data(dna_a, score_a),
1264
1320
  "prompt_b": _build_data(dna_b, score_b),
1265
1321
  }
1266
1322
 
1323
+ # Include prompt texts for --best-worst display
1324
+ if prompt_a_text:
1325
+ result["prompt_a_text"] = prompt_a_text
1326
+ result["prompt_b_text"] = prompt_b_text
1327
+
1267
1328
  if json_output:
1268
1329
  import json as json_mod
1269
1330
 
@@ -13,7 +13,10 @@ from __future__ import annotations
13
13
 
14
14
  import re
15
15
  from collections import Counter
16
- from typing import Any
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ if TYPE_CHECKING:
19
+ from reprompt.storage.db import PromptDB
17
20
 
18
21
  # Patterns that indicate specificity
19
22
  _FILE_REF = re.compile(r"[\w/]+\.\w{1,5}") # file.py, path/to/file.ts
@@ -105,3 +108,61 @@ def compute_style(prompts: list[dict[str, Any]]) -> dict[str, Any]:
105
108
  "specificity": round(specificity, 2),
106
109
  "length_distribution": length_dist,
107
110
  }
111
+
112
+
113
+ def compute_style_trends(
114
+ db: PromptDB,
115
+ period: str = "7d",
116
+ source: str | None = None,
117
+ ) -> dict[str, Any]:
118
+ """Compare style between current and previous period.
119
+
120
+ Returns dict with:
121
+ period, current (style dict), previous (style dict),
122
+ deltas: {specificity, avg_length, prompt_count,
123
+ top_category_changed, top_category_current, top_category_previous}
124
+ """
125
+ from reprompt.core.library import categorize_prompt
126
+ from reprompt.core.timeutil import sliding_windows
127
+
128
+ windows = sliding_windows(period=period, count=2)
129
+ prev_window = windows[0]
130
+ curr_window = windows[1]
131
+
132
+ def _build_prompts(window):
133
+ rows = db.get_prompts_in_range(
134
+ window.start.isoformat(),
135
+ window.end.isoformat(),
136
+ source=source,
137
+ )
138
+ return [
139
+ {
140
+ "text": r["text"],
141
+ "category": categorize_prompt(r["text"]),
142
+ "char_count": r.get("char_count", len(r["text"])),
143
+ }
144
+ for r in rows
145
+ if r.get("duplicate_of") is None
146
+ ]
147
+
148
+ prev_prompts = _build_prompts(prev_window)
149
+ curr_prompts = _build_prompts(curr_window)
150
+
151
+ previous = compute_style(prev_prompts)
152
+ current = compute_style(curr_prompts)
153
+
154
+ deltas: dict[str, Any] = {
155
+ "specificity": round(current["specificity"] - previous["specificity"], 2),
156
+ "avg_length": round(current["avg_length"] - previous["avg_length"], 1),
157
+ "prompt_count": current["prompt_count"] - previous["prompt_count"],
158
+ "top_category_changed": current["top_category"] != previous["top_category"],
159
+ "top_category_current": current["top_category"],
160
+ "top_category_previous": previous["top_category"],
161
+ }
162
+
163
+ return {
164
+ "period": period,
165
+ "current": current,
166
+ "previous": previous,
167
+ "deltas": deltas,
168
+ }
@@ -462,6 +462,18 @@ def render_compare(data: dict[str, Any]) -> str:
462
462
 
463
463
  console.print("\n[bold]Prompt Comparison[/bold]")
464
464
 
465
+ # Show prompt texts if provided (from --best-worst)
466
+ if "prompt_a_text" in data:
467
+ a_text = data["prompt_a_text"]
468
+ b_text = data["prompt_b_text"]
469
+
470
+ def _truncate(t: str) -> str:
471
+ return (t[:77] + "...") if len(t) > 80 else t
472
+
473
+ console.print(f" [green]Best:[/green] {_truncate(a_text)}")
474
+ console.print(f" [red]Worst:[/red] {_truncate(b_text)}")
475
+ console.print()
476
+
465
477
  table = Table()
466
478
  table.add_column("Feature", style="dim", min_width=18)
467
479
  table.add_column("Prompt A", justify="right")
@@ -657,6 +669,84 @@ def render_style(data: dict[str, Any]) -> str:
657
669
  return buf.getvalue()
658
670
 
659
671
 
672
+ def render_style_trends(data: dict[str, Any]) -> str:
673
+ """Render style trends comparison between two periods."""
674
+ buf = StringIO()
675
+ console = Console(file=buf, force_terminal=True, width=80)
676
+
677
+ curr = data["current"]
678
+ prev = data["previous"]
679
+ deltas = data["deltas"]
680
+
681
+ # Handle empty data
682
+ if curr["prompt_count"] == 0 and prev["prompt_count"] == 0:
683
+ console.print(
684
+ "\n[dim]Not enough data for trends. Keep prompting and check back next week.[/dim]"
685
+ )
686
+ return buf.getvalue()
687
+
688
+ if prev["prompt_count"] == 0:
689
+ console.print(f"\n[bold]Style Trends ({data['period']})[/bold]")
690
+ console.print("\u2500" * 40)
691
+ console.print(" [dim]New! No previous data to compare.[/dim]")
692
+ console.print(
693
+ f" Current: {curr['prompt_count']} prompts,"
694
+ f" specificity {curr['specificity']:.2f},"
695
+ f" avg {curr['avg_length']:.0f} chars"
696
+ )
697
+ return buf.getvalue()
698
+
699
+ console.print(f"\n[bold]Style Trends ({data['period']})[/bold]")
700
+ console.print("\u2500" * 40)
701
+
702
+ # Specificity (green = improvement)
703
+ spec_delta = deltas["specificity"]
704
+ spec_sign = "+" if spec_delta > 0 else ""
705
+ spec_pct = (
706
+ f" ({spec_sign}{spec_delta / prev['specificity'] * 100:.0f}%)"
707
+ if prev["specificity"] > 0
708
+ else ""
709
+ )
710
+ spec_color = "green" if spec_delta > 0 else "red" if spec_delta < 0 else "dim"
711
+ console.print(
712
+ f" Specificity {prev['specificity']:.2f}"
713
+ f" \u2192 {curr['specificity']:.2f}"
714
+ f" [{spec_color}]{spec_sign}{spec_delta:.2f}"
715
+ f"{spec_pct}[/{spec_color}]"
716
+ )
717
+
718
+ # Avg Length (neutral, always dim)
719
+ len_delta = deltas["avg_length"]
720
+ len_sign = "+" if len_delta > 0 else ""
721
+ console.print(
722
+ f" Avg Length {prev['avg_length']:.0f}"
723
+ f" \u2192 {curr['avg_length']:.0f} chars"
724
+ f" [dim]{len_sign}{len_delta:.0f} chars[/dim]"
725
+ )
726
+
727
+ # Prompt count (green = more activity)
728
+ count_delta = deltas["prompt_count"]
729
+ count_sign = "+" if count_delta > 0 else ""
730
+ count_color = "green" if count_delta > 0 else "dim"
731
+ console.print(
732
+ f" Prompts {prev['prompt_count']}"
733
+ f" \u2192 {curr['prompt_count']}"
734
+ f" [{count_color}]{count_sign}{count_delta}[/{count_color}]"
735
+ )
736
+
737
+ # Top category shift
738
+ if deltas["top_category_changed"]:
739
+ console.print(
740
+ f" Top Category {deltas['top_category_previous']}"
741
+ f" \u2192 {deltas['top_category_current']}"
742
+ )
743
+ else:
744
+ console.print(f" Top Category {deltas['top_category_current']} (unchanged)")
745
+
746
+ console.print()
747
+ return buf.getvalue()
748
+
749
+
660
750
  def render_privacy(data: dict[str, Any]) -> str:
661
751
  """Render privacy exposure summary."""
662
752
  buf = StringIO()
@@ -954,6 +954,46 @@ class PromptDB:
954
954
  finally:
955
955
  conn.close()
956
956
 
957
+ def get_best_worst_prompts(self, source: str | None = None) -> tuple[str, str] | None:
958
+ """Return (best_text, worst_text) from scored prompts.
959
+
960
+ Filters to prompts with >= 5 words to avoid noise.
961
+ Returns None if fewer than 2 qualifying prompts exist.
962
+ """
963
+ conn = self._conn()
964
+ try:
965
+ if source:
966
+ rows = conn.execute(
967
+ """SELECT p.text, pf.overall_score
968
+ FROM prompt_features pf
969
+ JOIN prompts p ON pf.prompt_hash = p.hash
970
+ WHERE pf.overall_score IS NOT NULL AND p.source = ?
971
+ ORDER BY pf.overall_score DESC""",
972
+ (source,),
973
+ ).fetchall()
974
+ else:
975
+ rows = conn.execute(
976
+ """SELECT p.text, pf.overall_score
977
+ FROM prompt_features pf
978
+ JOIN prompts p ON pf.prompt_hash = p.hash
979
+ WHERE pf.overall_score IS NOT NULL
980
+ ORDER BY pf.overall_score DESC""",
981
+ ).fetchall()
982
+
983
+ # Filter to prompts with >= 5 words
984
+ qualified = [
985
+ (row["text"], row["overall_score"]) for row in rows if len(row["text"].split()) >= 5
986
+ ]
987
+
988
+ if len(qualified) < 2:
989
+ return None
990
+
991
+ best_text = qualified[0][0] # highest score (first in DESC order)
992
+ worst_text = qualified[-1][0] # lowest score (last in DESC order)
993
+ return (best_text, worst_text)
994
+ finally:
995
+ conn.close()
996
+
957
997
  # -- digest_log ---------------------------------------------------------
958
998
 
959
999
  def log_digest(