reprompt-cli 1.8.0__tar.gz → 1.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/PKG-INFO +1 -1
  2. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/pyproject.toml +1 -1
  3. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/__init__.py +1 -1
  4. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/cli.py +51 -0
  5. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/insights.py +26 -0
  6. reprompt_cli-1.8.1/src/reprompt/core/repetition.py +128 -0
  7. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/session_quality.py +1 -3
  8. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/suggestions.py +3 -0
  9. reprompt_cli-1.8.1/src/reprompt/output/repetition_terminal.py +61 -0
  10. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/sessions_terminal.py +2 -8
  11. reprompt_cli-1.8.1/tests/test_repetition.py +124 -0
  12. reprompt_cli-1.8.1/tests/test_repetition_cli.py +102 -0
  13. reprompt_cli-1.8.1/tests/test_repetition_output.py +76 -0
  14. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_suggestions.py +9 -2
  15. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/uv.lock +1 -1
  16. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.editorconfig +0 -0
  17. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  18. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  19. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  20. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  21. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/dependabot.yml +0 -0
  23. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/workflows/ci.yml +0 -0
  24. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.github/workflows/publish.yml +0 -0
  25. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.gitignore +0 -0
  26. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.pre-commit-config.yaml +0 -0
  27. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.pre-commit-hooks.yaml +0 -0
  28. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.testmondata-shm +0 -0
  29. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/.testmondata-wal +0 -0
  30. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/CHANGELOG.md +0 -0
  31. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/CODE_OF_CONDUCT.md +0 -0
  32. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/CONTRIBUTING.md +0 -0
  33. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/LICENSE +0 -0
  34. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/README.md +0 -0
  35. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/SECURITY.md +0 -0
  36. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
  37. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/action.yml +0 -0
  38. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/demo.gif +0 -0
  39. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-128.png +0 -0
  40. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-16.png +0 -0
  41. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-256.png +0 -0
  42. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-32.png +0 -0
  43. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-48.png +0 -0
  44. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-512.png +0 -0
  45. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon-96.png +0 -0
  46. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/brand-icon.svg +0 -0
  47. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-128.png +0 -0
  48. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-16.png +0 -0
  49. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-256.png +0 -0
  50. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-32.png +0 -0
  51. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-48.png +0 -0
  52. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-512.png +0 -0
  53. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-96.png +0 -0
  54. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-favicon.svg +0 -0
  55. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-128.png +0 -0
  56. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-16.png +0 -0
  57. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-256.png +0 -0
  58. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-32.png +0 -0
  59. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-48.png +0 -0
  60. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-512.png +0 -0
  61. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon-96.png +0 -0
  62. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/cli-icon.svg +0 -0
  63. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-128.png +0 -0
  64. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-16.png +0 -0
  65. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-256.png +0 -0
  66. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-32.png +0 -0
  67. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-48.png +0 -0
  68. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-512.png +0 -0
  69. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon-96.png +0 -0
  70. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/favicon.svg +0 -0
  71. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/icons/generate.sh +0 -0
  72. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
  73. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
  74. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/scripts/generate_demo_data.py +0 -0
  75. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/__init__.py +0 -0
  76. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/aider.py +0 -0
  77. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/base.py +0 -0
  78. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/chatgpt.py +0 -0
  79. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/claude_chat.py +0 -0
  80. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/claude_code.py +0 -0
  81. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/cline.py +0 -0
  82. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/codex.py +0 -0
  83. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/cursor.py +0 -0
  84. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/filters.py +0 -0
  85. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/gemini.py +0 -0
  86. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/adapters/openclaw.py +0 -0
  87. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/__init__.py +0 -0
  88. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/handler.py +0 -0
  89. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/host.py +0 -0
  90. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/manifest.py +0 -0
  91. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/bridge/protocol.py +0 -0
  92. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/commands/__init__.py +0 -0
  93. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/commands/telemetry.py +0 -0
  94. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/commands/wrapped.py +0 -0
  95. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/config.py +0 -0
  96. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/__init__.py +0 -0
  97. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/agent.py +0 -0
  98. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/analyzer.py +0 -0
  99. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/compress.py +0 -0
  100. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/conversation.py +0 -0
  101. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/cost.py +0 -0
  102. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/dashboard.py +0 -0
  103. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/dedup.py +0 -0
  104. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/digest.py +0 -0
  105. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/distill.py +0 -0
  106. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/effectiveness.py +0 -0
  107. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/extractors.py +0 -0
  108. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/extractors_zh.py +0 -0
  109. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/lang_detect.py +0 -0
  110. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/library.py +0 -0
  111. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/lint.py +0 -0
  112. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/merge_view.py +0 -0
  113. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/models.py +0 -0
  114. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/persona.py +0 -0
  115. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/pipeline.py +0 -0
  116. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/privacy.py +0 -0
  117. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/privacy_scan.py +0 -0
  118. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/prompt_dna.py +0 -0
  119. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/recommend.py +0 -0
  120. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/scorer.py +0 -0
  121. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/segmenter.py +0 -0
  122. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/session_meta.py +0 -0
  123. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/session_type.py +0 -0
  124. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/style.py +0 -0
  125. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/templates.py +0 -0
  126. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/timeutil.py +0 -0
  127. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/trends.py +0 -0
  128. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/core/wrapped.py +0 -0
  129. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/demo.py +0 -0
  130. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/__init__.py +0 -0
  131. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/base.py +0 -0
  132. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/local_embed.py +0 -0
  133. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/ollama.py +0 -0
  134. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/openai_embed.py +0 -0
  135. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/embeddings/tfidf.py +0 -0
  136. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/mcp.py +0 -0
  137. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/mcp_main.py +0 -0
  138. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/__init__.py +0 -0
  139. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/agent_terminal.py +0 -0
  140. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/chartjs.min.js +0 -0
  141. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/compress_terminal.py +0 -0
  142. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/dashboard_terminal.py +0 -0
  143. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/distill_terminal.py +0 -0
  144. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/export.py +0 -0
  145. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/html_report.py +0 -0
  146. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/json_out.py +0 -0
  147. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/markdown.py +0 -0
  148. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/terminal.py +0 -0
  149. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/wrapped_html.py +0 -0
  150. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/output/wrapped_terminal.py +0 -0
  151. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/py.typed +0 -0
  152. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/sharing/__init__.py +0 -0
  153. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/sharing/client.py +0 -0
  154. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/sharing/clipboard.py +0 -0
  155. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/storage/__init__.py +0 -0
  156. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/storage/db.py +0 -0
  157. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/__init__.py +0 -0
  158. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/collector.py +0 -0
  159. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/consent.py +0 -0
  160. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/events.py +0 -0
  161. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/prompt.py +0 -0
  162. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/queue.py +0 -0
  163. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/src/reprompt/telemetry/sender.py +0 -0
  164. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/__init__.py +0 -0
  165. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/conftest.py +0 -0
  166. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/aider_chat_history.md +0 -0
  167. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/chatgpt_conversations.json +0 -0
  168. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/claude_chat_export.json +0 -0
  169. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/claude_session.jsonl +0 -0
  170. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
  171. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/export/default_export.md +0 -0
  172. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/export/full_export.md +0 -0
  173. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/gemini_session.json +0 -0
  174. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/fixtures/openclaw_session.jsonl +0 -0
  175. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_aider.py +0 -0
  176. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_chatgpt.py +0 -0
  177. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_claude.py +0 -0
  178. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_claude_chat.py +0 -0
  179. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_cline.py +0 -0
  180. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_gemini.py +0 -0
  181. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_adapter_openclaw.py +0 -0
  182. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_agent.py +0 -0
  183. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_agent_cli.py +0 -0
  184. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_analyzer.py +0 -0
  185. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_cli.py +0 -0
  186. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_e2e.py +0 -0
  187. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_handler.py +0 -0
  188. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_integration.py +0 -0
  189. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_manifest.py +0 -0
  190. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_bridge_protocol.py +0 -0
  191. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cli.py +0 -0
  192. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cli_deprecations.py +0 -0
  193. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cli_library_effectiveness.py +0 -0
  194. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_clipboard.py +0 -0
  195. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_codex_adapter.py +0 -0
  196. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compare_best_worst.py +0 -0
  197. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress.py +0 -0
  198. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress_cli.py +0 -0
  199. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress_dna.py +0 -0
  200. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress_html.py +0 -0
  201. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_compress_insights.py +0 -0
  202. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_config.py +0 -0
  203. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_conversation.py +0 -0
  204. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_copy_flag.py +0 -0
  205. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cost.py +0 -0
  206. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_coverage_boost.py +0 -0
  207. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_cursor_adapter.py +0 -0
  208. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_dashboard.py +0 -0
  209. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db.py +0 -0
  210. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db_digest.py +0 -0
  211. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db_effectiveness.py +0 -0
  212. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db_session_quality.py +0 -0
  213. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_db_trends.py +0 -0
  214. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_dedup.py +0 -0
  215. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_demo.py +0 -0
  216. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_deprecated_commands.py +0 -0
  217. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_digest.py +0 -0
  218. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_digest_cli.py +0 -0
  219. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_distill.py +0 -0
  220. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_distill_cli.py +0 -0
  221. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_distill_weights.py +0 -0
  222. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_e2e.py +0 -0
  223. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_effectiveness.py +0 -0
  224. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_embeddings_local.py +0 -0
  225. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_embeddings_ollama.py +0 -0
  226. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_embeddings_openai.py +0 -0
  227. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_empty_state.py +0 -0
  228. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_export.py +0 -0
  229. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_export_cli.py +0 -0
  230. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_export_snapshot.py +0 -0
  231. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_extractors.py +0 -0
  232. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_extractors_routing.py +0 -0
  233. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_extractors_zh.py +0 -0
  234. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_extractors_zh_e2e.py +0 -0
  235. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_html_report.py +0 -0
  236. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_import_cli.py +0 -0
  237. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_import_e2e.py +0 -0
  238. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_insights.py +0 -0
  239. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_insights_cli.py +0 -0
  240. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_insights_expanded.py +0 -0
  241. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_install_hook.py +0 -0
  242. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_lang_detect.py +0 -0
  243. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_library.py +0 -0
  244. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_lint.py +0 -0
  245. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_lint_cli.py +0 -0
  246. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_markdown.py +0 -0
  247. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_mcp.py +0 -0
  248. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_merge_view.py +0 -0
  249. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_models.py +0 -0
  250. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_output.py +0 -0
  251. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_parse_conversation_base.py +0 -0
  252. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_parse_conversation_chatgpt.py +0 -0
  253. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_parse_conversation_claude.py +0 -0
  254. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_persona.py +0 -0
  255. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_pipeline.py +0 -0
  256. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy.py +0 -0
  257. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy_cli.py +0 -0
  258. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy_e2e.py +0 -0
  259. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy_output.py +0 -0
  260. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_privacy_scan.py +0 -0
  261. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_prompt_dna.py +0 -0
  262. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_public_api.py +0 -0
  263. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_recommend.py +0 -0
  264. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_schema_version.py +0 -0
  265. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_score_cli.py +0 -0
  266. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_scorer.py +0 -0
  267. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_segmenter.py +0 -0
  268. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_session_quality.py +0 -0
  269. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_session_type.py +0 -0
  270. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_sessions_cli.py +0 -0
  271. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_sessions_output.py +0 -0
  272. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_share_e2e.py +0 -0
  273. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_sharing_client.py +0 -0
  274. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_source_filter.py +0 -0
  275. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_style.py +0 -0
  276. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_style_trends.py +0 -0
  277. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_cli.py +0 -0
  278. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_collector.py +0 -0
  279. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_consent.py +0 -0
  280. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_e2e.py +0 -0
  281. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_events.py +0 -0
  282. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_prompt.py +0 -0
  283. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_queue.py +0 -0
  284. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_telemetry_sender.py +0 -0
  285. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_template_cli.py +0 -0
  286. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_templates.py +0 -0
  287. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_timeutil.py +0 -0
  288. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_trends.py +0 -0
  289. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_trends_cli.py +0 -0
  290. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_use_cli.py +0 -0
  291. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped.py +0 -0
  292. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped_cli.py +0 -0
  293. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped_e2e.py +0 -0
  294. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped_html.py +0 -0
  295. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped_output.py +0 -0
  296. {reprompt_cli-1.8.0 → reprompt_cli-1.8.1}/tests/test_wrapped_share.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reprompt-cli
3
- Version: 1.8.0
3
+ Version: 1.8.1
4
4
  Summary: Discover, analyze, and optimize your prompts from AI coding sessions
5
5
  Project-URL: Homepage, https://github.com/reprompt-dev/reprompt
6
6
  Project-URL: Repository, https://github.com/reprompt-dev/reprompt
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reprompt-cli"
3
- version = "1.8.0"
3
+ version = "1.8.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.0"
3
+ __version__ = "1.8.1"
4
4
 
5
5
  __all__ = [
6
6
  "__version__",
@@ -1474,6 +1474,7 @@ def insights(
1474
1474
  from reprompt.config import Settings
1475
1475
  from reprompt.core.insights import (
1476
1476
  compute_insights,
1477
+ get_cross_session_repetition_insight,
1477
1478
  get_effectiveness_insight,
1478
1479
  get_similar_prompts_insight,
1479
1480
  )
@@ -1487,12 +1488,14 @@ def insights(
1487
1488
  # Expanded sections
1488
1489
  eff_data = get_effectiveness_insight(db, source=source)
1489
1490
  sim_data = get_similar_prompts_insight(db, source=source)
1491
+ rep_data = get_cross_session_repetition_insight(db, source=source)
1490
1492
 
1491
1493
  if json_output:
1492
1494
  import json as json_mod
1493
1495
 
1494
1496
  result["effectiveness"] = eff_data
1495
1497
  result["similar_prompts"] = sim_data
1498
+ result["cross_session_repetition"] = rep_data
1496
1499
  typer.echo(json_mod.dumps(result, indent=2))
1497
1500
  else:
1498
1501
  from reprompt.core.suggestions import get_suggestion
@@ -1516,6 +1519,15 @@ def insights(
1516
1519
  ' [dim]\u2192 Try: reprompt template save "..." (reuse instead of rewrite)[/dim]'
1517
1520
  )
1518
1521
 
1522
+ if rep_data:
1523
+ rate_pct = f"{rep_data['repetition_rate'] * 100:.0f}%"
1524
+ n = rep_data["total_recurring_topics"]
1525
+ console.print("\n [bold]Cross-Session Repetition[/bold]")
1526
+ console.print(f" {rate_pct} of prompts recur across sessions ({n} topics)")
1527
+ for t in rep_data["top_topics"]:
1528
+ console.print(f' "{t["canonical_text"]}" \u2014 {t["session_count"]} sessions')
1529
+ console.print(" [dim]\u2192 Try: reprompt repetition (full analysis)[/dim]")
1530
+
1519
1531
  hint = get_suggestion("insights")
1520
1532
  if hint:
1521
1533
  console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
@@ -1525,6 +1537,7 @@ def insights(
1525
1537
 
1526
1538
  result["effectiveness"] = eff_data
1527
1539
  result["similar_prompts"] = sim_data
1540
+ result["cross_session_repetition"] = rep_data
1528
1541
  _copy_to_clip(json_mod.dumps(result, indent=2), quiet=json_output)
1529
1542
 
1530
1543
 
@@ -1974,6 +1987,44 @@ def sessions(
1974
1987
  _copy_to_clip(copy_text, quiet=json_output)
1975
1988
 
1976
1989
 
1990
+ @app.command(rich_help_panel="Analyze")
1991
+ def repetition(
1992
+ last: int = typer.Option(500, "--last", help="Analyze N most recent unique prompts"),
1993
+ source: str = typer.Option(
1994
+ None, "--source", "-s", help="Filter by source (e.g. claude-code, cursor)"
1995
+ ),
1996
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
1997
+ copy: bool = typer.Option(False, "--copy", help="Copy result to clipboard"),
1998
+ ) -> None:
1999
+ """Detect recurring prompts across different sessions."""
2000
+ import json as json_mod
2001
+ from dataclasses import asdict
2002
+
2003
+ from reprompt.config import Settings
2004
+ from reprompt.core.repetition import analyze_repetition
2005
+ from reprompt.output.repetition_terminal import render_repetition_report
2006
+ from reprompt.storage.db import PromptDB
2007
+
2008
+ settings = Settings()
2009
+ db = PromptDB(settings.db_path)
2010
+ report = analyze_repetition(db, source=source, limit=last)
2011
+
2012
+ if json_output:
2013
+ typer.echo(json_mod.dumps(asdict(report), indent=2, default=str))
2014
+ else:
2015
+ typer.echo(render_repetition_report(report), nl=False)
2016
+
2017
+ from reprompt.core.suggestions import get_suggestion
2018
+
2019
+ hint = get_suggestion("repetition")
2020
+ if hint:
2021
+ console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
2022
+
2023
+ if copy:
2024
+ copy_text = json_mod.dumps(asdict(report), indent=2, default=str)
2025
+ _copy_to_clip(copy_text, quiet=json_output)
2026
+
2027
+
1977
2028
  @app.command(rich_help_panel="Analyze")
1978
2029
  def agent(
1979
2030
  last: int = typer.Option(5, "--last", help="Analyze N most recent sessions"),
@@ -293,3 +293,29 @@ def get_similar_prompts_insight(
293
293
  "total_clusters": len(clusters),
294
294
  "total_clustered_prompts": sum(c["size"] for c in clusters),
295
295
  }
296
+
297
+
298
+ def get_cross_session_repetition_insight(
299
+ db: PromptDB,
300
+ source: str | None = None,
301
+ ) -> dict[str, Any] | None:
302
+ """Return cross-session repetition summary, or None if no recurring topics."""
303
+ from reprompt.core.repetition import analyze_repetition
304
+
305
+ report = analyze_repetition(db, source=source, limit=500)
306
+
307
+ if not report.recurring_topics:
308
+ return None
309
+
310
+ return {
311
+ "repetition_rate": report.repetition_rate,
312
+ "top_topics": [
313
+ {
314
+ "canonical_text": t.canonical_text[:80],
315
+ "session_count": t.session_count,
316
+ "total_matches": t.total_matches,
317
+ }
318
+ for t in report.recurring_topics[:3]
319
+ ],
320
+ "total_recurring_topics": len(report.recurring_topics),
321
+ }
@@ -0,0 +1,128 @@
1
+ """Cross-session prompt repetition detection.
2
+
3
+ Identifies recurring topics asked across different AI coding sessions
4
+ using TF-IDF + containment similarity clustering. All analysis is
5
+ rule-based (zero LLM).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+ from reprompt.storage.db import PromptDB
14
+
15
+
16
+ @dataclass
17
+ class RecurringTopic:
18
+ """A topic that recurs across multiple sessions."""
19
+
20
+ canonical_text: str
21
+ session_count: int
22
+ total_matches: int
23
+ session_ids: list[str] = field(default_factory=list)
24
+ earliest: str = ""
25
+ latest: str = ""
26
+
27
+
28
+ @dataclass
29
+ class RepetitionReport:
30
+ """Result of cross-session repetition analysis."""
31
+
32
+ total_prompts_analyzed: int = 0
33
+ cross_session_matches: int = 0
34
+ repetition_rate: float = 0.0 # cross_session_matches / total
35
+ recurring_topics: list[RecurringTopic] = field(default_factory=list)
36
+ total_sessions: int = 0
37
+
38
+
39
+ def analyze_repetition(
40
+ db: PromptDB,
41
+ source: str | None = None,
42
+ limit: int = 500,
43
+ threshold: float = 0.75,
44
+ ) -> RepetitionReport:
45
+ """Detect recurring prompts across different sessions.
46
+
47
+ Reuses merge_view.build_clusters() for similarity, then filters
48
+ to clusters spanning 2+ distinct sessions.
49
+ """
50
+ from reprompt.core.merge_view import build_clusters
51
+
52
+ all_prompts = db.get_all_prompts(source=source)
53
+
54
+ # Filter to unique prompts only
55
+ unique = [p for p in all_prompts if p.get("duplicate_of") is None]
56
+
57
+ if not unique:
58
+ return RepetitionReport()
59
+
60
+ # Limit to most recent N (by id desc), then reverse for chronological
61
+ unique.sort(key=lambda p: p.get("id", 0), reverse=True)
62
+ unique = unique[:limit]
63
+ unique.reverse()
64
+
65
+ # Build lookup: text → prompt dict (safe due to hash uniqueness)
66
+ text_to_prompt: dict[str, dict[str, Any]] = {}
67
+ for p in unique:
68
+ text_to_prompt[p["text"]] = p
69
+
70
+ texts = [p["text"] for p in unique]
71
+ timestamps = [p.get("timestamp", "") for p in unique]
72
+ all_session_ids = {p.get("session_id") or "unknown" for p in unique}
73
+
74
+ # Build clusters using existing infrastructure
75
+ clusters = build_clusters(texts, timestamps, threshold=threshold)
76
+
77
+ # Filter to cross-session clusters
78
+ recurring: list[RecurringTopic] = []
79
+ total_cross_matches = 0
80
+
81
+ for cluster in clusters:
82
+ # Collect all texts in cluster (canonical + members)
83
+ cluster_texts = [cluster["canonical"]["text"]]
84
+ cluster_texts.extend(m["text"] for m in cluster["members"])
85
+
86
+ # Map to session_ids
87
+ sids: list[str] = []
88
+ cluster_timestamps: list[str] = []
89
+ for t in cluster_texts:
90
+ prompt = text_to_prompt.get(t)
91
+ if prompt:
92
+ sids.append(prompt.get("session_id") or "unknown")
93
+ cluster_timestamps.append(prompt.get("timestamp", ""))
94
+
95
+ distinct_sessions = sorted(set(sids))
96
+ if len(distinct_sessions) < 2:
97
+ continue
98
+
99
+ # Build timestamps for range
100
+ valid_ts = sorted(t for t in cluster_timestamps if t)
101
+ earliest = valid_ts[0] if valid_ts else ""
102
+ latest = valid_ts[-1] if valid_ts else ""
103
+
104
+ recurring.append(
105
+ RecurringTopic(
106
+ canonical_text=cluster["canonical"]["text"],
107
+ session_count=len(distinct_sessions),
108
+ total_matches=len(cluster_texts),
109
+ session_ids=distinct_sessions,
110
+ earliest=earliest,
111
+ latest=latest,
112
+ )
113
+ )
114
+ total_cross_matches += len(cluster_texts)
115
+
116
+ # Sort by session_count desc, then total_matches desc
117
+ recurring.sort(key=lambda t: (-t.session_count, -t.total_matches))
118
+
119
+ total = len(unique)
120
+ rate = total_cross_matches / total if total > 0 else 0.0
121
+
122
+ return RepetitionReport(
123
+ total_prompts_analyzed=total,
124
+ cross_session_matches=total_cross_matches,
125
+ repetition_rate=round(rate, 3),
126
+ recurring_topics=recurring,
127
+ total_sessions=len(all_session_ids),
128
+ )
@@ -82,9 +82,7 @@ def _detect_frustration(turns: list[ConversationTurn]) -> FrustrationSignals:
82
82
  escalation = second_rate > first_rate * 1.5 and second_rate > 0.2
83
83
 
84
84
  # Stall turns: assistant turns with no tool calls and short text
85
- stall_turns = sum(
86
- 1 for t in asst_turns if t.tool_calls == 0 and len(t.text.strip()) < 50
87
- )
85
+ stall_turns = sum(1 for t in asst_turns if t.tool_calls == 0 and len(t.text.strip()) < 50)
88
86
 
89
87
  return FrustrationSignals(
90
88
  abandonment=abandonment,
@@ -18,6 +18,9 @@ SUGGESTIONS: dict[str, str] = {
18
18
  "sessions": (
19
19
  "reprompt sessions --detail <id> (deep-dive) · reprompt agent (error loop analysis)"
20
20
  ),
21
+ "repetition": (
22
+ 'reprompt template save "..." (reuse patterns) · reprompt insights (all patterns)'
23
+ ),
21
24
  "template": "reprompt insights (see which patterns work best)",
22
25
  }
23
26
 
@@ -0,0 +1,61 @@
1
+ """Rich terminal rendering for cross-session repetition analysis."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from io import StringIO
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from reprompt.core.repetition import RepetitionReport
12
+
13
+
14
+ def render_repetition_report(report: RepetitionReport) -> str:
15
+ """Render repetition report as formatted terminal output."""
16
+ console = Console(record=True, width=100, file=StringIO())
17
+
18
+ if not report.recurring_topics:
19
+ console.print("[dim]No cross-session repetition detected.[/dim]")
20
+ console.print(
21
+ "This means your prompts across sessions are unique — no recurring patterns found."
22
+ )
23
+ return console.export_text()
24
+
25
+ rate_pct = f"{report.repetition_rate * 100:.0f}%"
26
+ header = (
27
+ f"Prompts: {report.total_prompts_analyzed} | "
28
+ f"Sessions: {report.total_sessions} | "
29
+ f"Repetition Rate: {rate_pct}"
30
+ )
31
+ console.print(Panel(header, title="Cross-Session Repetition", border_style="cyan"))
32
+
33
+ # Table of recurring topics
34
+ table = Table(show_header=True, header_style="bold", padding=(0, 1))
35
+ table.add_column("Topic", max_width=45)
36
+ table.add_column("Sessions", justify="right", width=8)
37
+ table.add_column("Matches", justify="right", width=7)
38
+ table.add_column("Range", max_width=25)
39
+
40
+ for topic in report.recurring_topics[:10]:
41
+ text = topic.canonical_text
42
+ if len(text) > 45:
43
+ text = text[:42] + "..."
44
+
45
+ date_range = ""
46
+ if topic.earliest and topic.latest:
47
+ start = topic.earliest[:10]
48
+ end = topic.latest[:10]
49
+ date_range = f"{start} \u2192 {end}" if start != end else start
50
+
51
+ table.add_row(
52
+ text,
53
+ str(topic.session_count),
54
+ str(topic.total_matches),
55
+ date_range,
56
+ )
57
+
58
+ console.print(table)
59
+ console.print()
60
+
61
+ return console.export_text()
@@ -49,9 +49,7 @@ def render_sessions_table(sessions: list[dict[str, Any]]) -> str:
49
49
 
50
50
  if not sessions:
51
51
  console.print("[dim]No sessions with quality scores found.[/dim]")
52
- console.print(
53
- "Run [bold cyan]reprompt scan[/bold cyan] to import and score sessions."
54
- )
52
+ console.print("Run [bold cyan]reprompt scan[/bold cyan] to import and score sessions.")
55
53
  return console.export_text()
56
54
 
57
55
  # Compute avg quality
@@ -59,11 +57,7 @@ def render_sessions_table(sessions: list[dict[str, Any]]) -> str:
59
57
  avg_q = sum(s["quality_score"] for s in scored) / len(scored) if scored else 0
60
58
 
61
59
  # Header
62
- header = (
63
- f"Sessions: {len(sessions)} | "
64
- f"Scored: {len(scored)} | "
65
- f"Avg Quality: {avg_q:.0f}/100"
66
- )
60
+ header = f"Sessions: {len(sessions)} | Scored: {len(scored)} | Avg Quality: {avg_q:.0f}/100"
67
61
  console.print(Panel(header, title="Session Quality", border_style="cyan"))
68
62
 
69
63
  # Table
@@ -0,0 +1,124 @@
1
+ """Tests for cross-session prompt repetition detection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ from reprompt.core.repetition import analyze_repetition
10
+ from reprompt.storage.db import PromptDB
11
+
12
+
13
+ @pytest.fixture
14
+ def db(tmp_path: Path) -> PromptDB:
15
+ return PromptDB(tmp_path / "test.db")
16
+
17
+
18
+ def _insert(db: PromptDB, text: str, session_id: str, source: str = "claude-code") -> None:
19
+ db.insert_prompt(text, source=source, session_id=session_id, timestamp="2026-03-28T10:00:00Z")
20
+
21
+
22
+ class TestAnalyzeRepetition:
23
+ def test_empty_db_returns_zero(self, db: PromptDB):
24
+ report = analyze_repetition(db)
25
+ assert report.total_prompts_analyzed == 0
26
+ assert report.repetition_rate == 0.0
27
+ assert report.recurring_topics == []
28
+
29
+ def test_single_session_no_cross_session(self, db: PromptDB):
30
+ """Similar prompts in the same session don't count as cross-session."""
31
+ _insert(db, "fix the authentication bug in login.py", "s1")
32
+ _insert(db, "fix the authentication issue in login module", "s1")
33
+ _insert(db, "fix the auth problem in the login file", "s1")
34
+ report = analyze_repetition(db)
35
+ assert report.repetition_rate == 0.0
36
+ assert len(report.recurring_topics) == 0
37
+
38
+ def test_cross_session_detected(self, db: PromptDB):
39
+ """Similar prompts across sessions form a recurring topic."""
40
+ _insert(db, "fix the authentication bug in login.py please", "s1")
41
+ _insert(db, "fix the authentication bug in login.py now", "s2")
42
+ report = analyze_repetition(db)
43
+ assert len(report.recurring_topics) >= 1
44
+ topic = report.recurring_topics[0]
45
+ assert topic.session_count >= 2
46
+ assert topic.total_matches >= 2
47
+ assert report.repetition_rate > 0
48
+
49
+ def test_unrelated_prompts_no_match(self, db: PromptDB):
50
+ """Completely different prompts don't cluster."""
51
+ _insert(db, "fix the authentication bug in login.py", "s1")
52
+ _insert(db, "add pagination to the user list API endpoint", "s2")
53
+ report = analyze_repetition(db)
54
+ assert report.repetition_rate == 0.0
55
+
56
+ def test_three_sessions_same_topic(self, db: PromptDB):
57
+ """Topic spanning 3 sessions gets session_count=3."""
58
+ _insert(db, "fix the authentication bug in login.py", "s1")
59
+ _insert(db, "fix the authentication issue in login module", "s2")
60
+ _insert(db, "fix auth problem in the login file", "s3")
61
+ report = analyze_repetition(db)
62
+ if report.recurring_topics:
63
+ topic = report.recurring_topics[0]
64
+ assert topic.session_count >= 2 # at least 2, ideally 3
65
+
66
+ def test_duplicate_of_excluded(self, db: PromptDB):
67
+ """Prompts marked as duplicates should be excluded."""
68
+ _insert(db, "fix the authentication bug in login.py", "s1")
69
+ _insert(db, "fix the authentication bug in login.py copy", "s2")
70
+ # Mark second as duplicate
71
+ conn = db._conn()
72
+ try:
73
+ conn.execute("UPDATE prompts SET duplicate_of = 1 WHERE session_id = 's2'")
74
+ conn.commit()
75
+ finally:
76
+ conn.close()
77
+ report = analyze_repetition(db)
78
+ assert report.total_prompts_analyzed == 1 # only the non-duplicate
79
+
80
+ def test_limit_caps_analysis(self, db: PromptDB):
81
+ """Limit parameter restricts how many prompts are analyzed."""
82
+ for i in range(10):
83
+ _insert(db, f"unique prompt number {i} about topic {i}", f"s{i}")
84
+ report = analyze_repetition(db, limit=5)
85
+ assert report.total_prompts_analyzed == 5
86
+
87
+ def test_sorted_by_session_count(self, db: PromptDB):
88
+ """Topics sorted by session_count descending."""
89
+ # Topic A: 3 sessions
90
+ _insert(db, "fix the authentication bug in login.py", "s1")
91
+ _insert(db, "fix the authentication issue in login module", "s2")
92
+ _insert(db, "fix the auth problem in login file", "s3")
93
+ # Topic B: 2 sessions (different topic)
94
+ _insert(db, "add comprehensive unit tests for the payment API", "s4")
95
+ _insert(db, "add unit tests for the payment API endpoint", "s5")
96
+ report = analyze_repetition(db)
97
+ if len(report.recurring_topics) >= 2:
98
+ assert (
99
+ report.recurring_topics[0].session_count >= report.recurring_topics[1].session_count
100
+ )
101
+
102
+ def test_source_filter(self, db: PromptDB):
103
+ """Source filter limits analysis to specific adapter."""
104
+ _insert(db, "fix the authentication bug in login.py", "s1", source="claude-code")
105
+ _insert(db, "fix the authentication issue in login", "s2", source="cursor")
106
+ report = analyze_repetition(db, source="claude-code")
107
+ assert report.total_prompts_analyzed == 1
108
+
109
+ def test_total_sessions_counted(self, db: PromptDB):
110
+ """Total sessions reflects distinct session count in scope."""
111
+ _insert(db, "prompt one about topic alpha", "s1")
112
+ _insert(db, "prompt two about topic beta", "s2")
113
+ _insert(db, "prompt three about topic gamma", "s3")
114
+ report = analyze_repetition(db)
115
+ assert report.total_sessions == 3
116
+
117
+ def test_report_fields_present(self, db: PromptDB):
118
+ """All RepetitionReport fields are accessible."""
119
+ report = analyze_repetition(db)
120
+ assert isinstance(report.total_prompts_analyzed, int)
121
+ assert isinstance(report.cross_session_matches, int)
122
+ assert isinstance(report.repetition_rate, float)
123
+ assert isinstance(report.recurring_topics, list)
124
+ assert isinstance(report.total_sessions, int)
@@ -0,0 +1,102 @@
1
+ """Tests for `reprompt repetition` CLI command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from typer.testing import CliRunner
9
+
10
+ from reprompt.cli import app
11
+
12
+ runner = CliRunner()
13
+
14
+
15
+ def _seed_db(tmp_path: Path) -> Path:
16
+ """Create a DB with cross-session similar prompts."""
17
+ from reprompt.storage.db import PromptDB
18
+
19
+ db_path = tmp_path / "test.db"
20
+ db = PromptDB(db_path)
21
+
22
+ # Topic A: auth bug across 2 sessions
23
+ db.insert_prompt(
24
+ "fix the authentication bug in login.py please",
25
+ source="claude-code",
26
+ session_id="s1",
27
+ timestamp="2026-03-01T10:00:00Z",
28
+ )
29
+ db.insert_prompt(
30
+ "fix the authentication bug in login.py now",
31
+ source="claude-code",
32
+ session_id="s2",
33
+ timestamp="2026-03-15T10:00:00Z",
34
+ )
35
+ # Unrelated prompt
36
+ db.insert_prompt(
37
+ "add pagination to the user list API endpoint",
38
+ source="claude-code",
39
+ session_id="s3",
40
+ timestamp="2026-03-20T10:00:00Z",
41
+ )
42
+ return db_path
43
+
44
+
45
+ def test_repetition_no_data(tmp_path, monkeypatch):
46
+ db_path = tmp_path / "empty.db"
47
+ monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
48
+ result = runner.invoke(app, ["repetition"])
49
+ assert result.exit_code == 0
50
+ assert "no cross-session" in result.output.lower()
51
+
52
+
53
+ def test_repetition_no_data_json(tmp_path, monkeypatch):
54
+ db_path = tmp_path / "empty.db"
55
+ monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
56
+ result = runner.invoke(app, ["repetition", "--json"])
57
+ assert result.exit_code == 0
58
+ data = json.loads(result.output)
59
+ assert data["repetition_rate"] == 0.0
60
+ assert data["recurring_topics"] == []
61
+
62
+
63
+ def test_repetition_with_data(tmp_path, monkeypatch):
64
+ db_path = _seed_db(tmp_path)
65
+ monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
66
+ result = runner.invoke(app, ["repetition"])
67
+ assert result.exit_code == 0
68
+
69
+
70
+ def test_repetition_json_output(tmp_path, monkeypatch):
71
+ db_path = _seed_db(tmp_path)
72
+ monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
73
+ result = runner.invoke(app, ["repetition", "--json"])
74
+ assert result.exit_code == 0
75
+ data = json.loads(result.output)
76
+ assert "repetition_rate" in data
77
+ assert "recurring_topics" in data
78
+ assert "total_prompts_analyzed" in data
79
+
80
+
81
+ def test_repetition_source_filter(tmp_path, monkeypatch):
82
+ db_path = _seed_db(tmp_path)
83
+ monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
84
+ result = runner.invoke(app, ["repetition", "--source", "nonexistent", "--json"])
85
+ assert result.exit_code == 0
86
+ data = json.loads(result.output)
87
+ assert data["total_prompts_analyzed"] == 0
88
+
89
+
90
+ def test_repetition_last_option(tmp_path, monkeypatch):
91
+ db_path = _seed_db(tmp_path)
92
+ monkeypatch.setenv("REPROMPT_DB_PATH", str(db_path))
93
+ result = runner.invoke(app, ["repetition", "--last", "2", "--json"])
94
+ assert result.exit_code == 0
95
+ data = json.loads(result.output)
96
+ assert data["total_prompts_analyzed"] <= 2
97
+
98
+
99
+ def test_repetition_help():
100
+ result = runner.invoke(app, ["repetition", "--help"])
101
+ assert result.exit_code == 0
102
+ assert "recurring" in result.output.lower() or "repetition" in result.output.lower()