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