reprompt-cli 1.7.1__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.7.1 → reprompt_cli-1.8.1}/PKG-INFO +3 -2
  2. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/README.md +2 -1
  3. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/pyproject.toml +1 -1
  4. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/__init__.py +1 -1
  5. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/cli.py +110 -1
  6. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/insights.py +26 -0
  7. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/pipeline.py +99 -0
  8. reprompt_cli-1.8.1/src/reprompt/core/repetition.py +128 -0
  9. reprompt_cli-1.8.1/src/reprompt/core/session_quality.py +201 -0
  10. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/suggestions.py +6 -0
  11. reprompt_cli-1.8.1/src/reprompt/output/repetition_terminal.py +61 -0
  12. reprompt_cli-1.8.1/src/reprompt/output/sessions_terminal.py +166 -0
  13. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/storage/db.py +123 -1
  14. reprompt_cli-1.8.1/tests/test_db_session_quality.py +181 -0
  15. reprompt_cli-1.8.1/tests/test_repetition.py +124 -0
  16. reprompt_cli-1.8.1/tests/test_repetition_cli.py +102 -0
  17. reprompt_cli-1.8.1/tests/test_repetition_output.py +76 -0
  18. reprompt_cli-1.8.1/tests/test_session_quality.py +455 -0
  19. reprompt_cli-1.8.1/tests/test_sessions_cli.py +193 -0
  20. reprompt_cli-1.8.1/tests/test_sessions_output.py +102 -0
  21. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_suggestions.py +12 -1
  22. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/uv.lock +1 -1
  23. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.editorconfig +0 -0
  24. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  25. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  26. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  27. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  28. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/dependabot.yml +0 -0
  30. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/workflows/ci.yml +0 -0
  31. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/workflows/publish.yml +0 -0
  32. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.gitignore +0 -0
  33. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.pre-commit-config.yaml +0 -0
  34. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.pre-commit-hooks.yaml +0 -0
  35. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.testmondata-shm +0 -0
  36. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.testmondata-wal +0 -0
  37. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/CHANGELOG.md +0 -0
  38. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/CODE_OF_CONDUCT.md +0 -0
  39. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/CONTRIBUTING.md +0 -0
  40. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/LICENSE +0 -0
  41. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/SECURITY.md +0 -0
  42. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
  43. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/action.yml +0 -0
  44. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/demo.gif +0 -0
  45. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-128.png +0 -0
  46. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-16.png +0 -0
  47. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-256.png +0 -0
  48. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-32.png +0 -0
  49. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-48.png +0 -0
  50. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-512.png +0 -0
  51. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-96.png +0 -0
  52. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon.svg +0 -0
  53. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-128.png +0 -0
  54. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-16.png +0 -0
  55. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-256.png +0 -0
  56. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-32.png +0 -0
  57. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-48.png +0 -0
  58. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-512.png +0 -0
  59. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-96.png +0 -0
  60. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon.svg +0 -0
  61. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-128.png +0 -0
  62. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-16.png +0 -0
  63. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-256.png +0 -0
  64. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-32.png +0 -0
  65. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-48.png +0 -0
  66. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-512.png +0 -0
  67. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-96.png +0 -0
  68. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon.svg +0 -0
  69. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-128.png +0 -0
  70. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-16.png +0 -0
  71. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-256.png +0 -0
  72. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-32.png +0 -0
  73. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-48.png +0 -0
  74. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-512.png +0 -0
  75. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-96.png +0 -0
  76. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon.svg +0 -0
  77. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/generate.sh +0 -0
  78. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
  79. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
  80. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/scripts/generate_demo_data.py +0 -0
  81. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/__init__.py +0 -0
  82. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/aider.py +0 -0
  83. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/base.py +0 -0
  84. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/chatgpt.py +0 -0
  85. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/claude_chat.py +0 -0
  86. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/claude_code.py +0 -0
  87. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/cline.py +0 -0
  88. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/codex.py +0 -0
  89. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/cursor.py +0 -0
  90. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/filters.py +0 -0
  91. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/gemini.py +0 -0
  92. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/openclaw.py +0 -0
  93. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/__init__.py +0 -0
  94. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/handler.py +0 -0
  95. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/host.py +0 -0
  96. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/manifest.py +0 -0
  97. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/protocol.py +0 -0
  98. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/commands/__init__.py +0 -0
  99. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/commands/telemetry.py +0 -0
  100. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/commands/wrapped.py +0 -0
  101. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/config.py +0 -0
  102. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/__init__.py +0 -0
  103. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/agent.py +0 -0
  104. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/analyzer.py +0 -0
  105. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/compress.py +0 -0
  106. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/conversation.py +0 -0
  107. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/cost.py +0 -0
  108. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/dashboard.py +0 -0
  109. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/dedup.py +0 -0
  110. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/digest.py +0 -0
  111. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/distill.py +0 -0
  112. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/effectiveness.py +0 -0
  113. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/extractors.py +0 -0
  114. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/extractors_zh.py +0 -0
  115. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/lang_detect.py +0 -0
  116. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/library.py +0 -0
  117. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/lint.py +0 -0
  118. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/merge_view.py +0 -0
  119. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/models.py +0 -0
  120. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/persona.py +0 -0
  121. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/privacy.py +0 -0
  122. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/privacy_scan.py +0 -0
  123. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/prompt_dna.py +0 -0
  124. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/recommend.py +0 -0
  125. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/scorer.py +0 -0
  126. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/segmenter.py +0 -0
  127. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/session_meta.py +0 -0
  128. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/session_type.py +0 -0
  129. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/style.py +0 -0
  130. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/templates.py +0 -0
  131. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/timeutil.py +0 -0
  132. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/trends.py +0 -0
  133. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/wrapped.py +0 -0
  134. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/demo.py +0 -0
  135. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/__init__.py +0 -0
  136. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/base.py +0 -0
  137. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/local_embed.py +0 -0
  138. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/ollama.py +0 -0
  139. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/openai_embed.py +0 -0
  140. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/tfidf.py +0 -0
  141. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/mcp.py +0 -0
  142. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/mcp_main.py +0 -0
  143. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/__init__.py +0 -0
  144. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/agent_terminal.py +0 -0
  145. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/chartjs.min.js +0 -0
  146. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/compress_terminal.py +0 -0
  147. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/dashboard_terminal.py +0 -0
  148. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/distill_terminal.py +0 -0
  149. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/export.py +0 -0
  150. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/html_report.py +0 -0
  151. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/json_out.py +0 -0
  152. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/markdown.py +0 -0
  153. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/terminal.py +0 -0
  154. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/wrapped_html.py +0 -0
  155. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/wrapped_terminal.py +0 -0
  156. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/py.typed +0 -0
  157. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/sharing/__init__.py +0 -0
  158. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/sharing/client.py +0 -0
  159. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/sharing/clipboard.py +0 -0
  160. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/storage/__init__.py +0 -0
  161. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/__init__.py +0 -0
  162. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/collector.py +0 -0
  163. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/consent.py +0 -0
  164. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/events.py +0 -0
  165. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/prompt.py +0 -0
  166. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/queue.py +0 -0
  167. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/sender.py +0 -0
  168. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/__init__.py +0 -0
  169. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/conftest.py +0 -0
  170. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/aider_chat_history.md +0 -0
  171. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/chatgpt_conversations.json +0 -0
  172. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/claude_chat_export.json +0 -0
  173. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/claude_session.jsonl +0 -0
  174. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
  175. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/export/default_export.md +0 -0
  176. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/export/full_export.md +0 -0
  177. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/gemini_session.json +0 -0
  178. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/openclaw_session.jsonl +0 -0
  179. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_aider.py +0 -0
  180. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_chatgpt.py +0 -0
  181. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_claude.py +0 -0
  182. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_claude_chat.py +0 -0
  183. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_cline.py +0 -0
  184. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_gemini.py +0 -0
  185. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_openclaw.py +0 -0
  186. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_agent.py +0 -0
  187. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_agent_cli.py +0 -0
  188. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_analyzer.py +0 -0
  189. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_cli.py +0 -0
  190. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_e2e.py +0 -0
  191. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_handler.py +0 -0
  192. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_integration.py +0 -0
  193. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_manifest.py +0 -0
  194. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_protocol.py +0 -0
  195. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cli.py +0 -0
  196. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cli_deprecations.py +0 -0
  197. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cli_library_effectiveness.py +0 -0
  198. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_clipboard.py +0 -0
  199. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_codex_adapter.py +0 -0
  200. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compare_best_worst.py +0 -0
  201. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress.py +0 -0
  202. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress_cli.py +0 -0
  203. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress_dna.py +0 -0
  204. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress_html.py +0 -0
  205. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress_insights.py +0 -0
  206. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_config.py +0 -0
  207. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_conversation.py +0 -0
  208. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_copy_flag.py +0 -0
  209. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cost.py +0 -0
  210. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_coverage_boost.py +0 -0
  211. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cursor_adapter.py +0 -0
  212. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_dashboard.py +0 -0
  213. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_db.py +0 -0
  214. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_db_digest.py +0 -0
  215. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_db_effectiveness.py +0 -0
  216. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_db_trends.py +0 -0
  217. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_dedup.py +0 -0
  218. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_demo.py +0 -0
  219. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_deprecated_commands.py +0 -0
  220. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_digest.py +0 -0
  221. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_digest_cli.py +0 -0
  222. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_distill.py +0 -0
  223. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_distill_cli.py +0 -0
  224. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_distill_weights.py +0 -0
  225. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_e2e.py +0 -0
  226. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_effectiveness.py +0 -0
  227. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_embeddings_local.py +0 -0
  228. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_embeddings_ollama.py +0 -0
  229. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_embeddings_openai.py +0 -0
  230. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_empty_state.py +0 -0
  231. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_export.py +0 -0
  232. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_export_cli.py +0 -0
  233. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_export_snapshot.py +0 -0
  234. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_extractors.py +0 -0
  235. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_extractors_routing.py +0 -0
  236. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_extractors_zh.py +0 -0
  237. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_extractors_zh_e2e.py +0 -0
  238. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_html_report.py +0 -0
  239. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_import_cli.py +0 -0
  240. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_import_e2e.py +0 -0
  241. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_insights.py +0 -0
  242. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_insights_cli.py +0 -0
  243. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_insights_expanded.py +0 -0
  244. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_install_hook.py +0 -0
  245. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_lang_detect.py +0 -0
  246. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_library.py +0 -0
  247. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_lint.py +0 -0
  248. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_lint_cli.py +0 -0
  249. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_markdown.py +0 -0
  250. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_mcp.py +0 -0
  251. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_merge_view.py +0 -0
  252. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_models.py +0 -0
  253. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_output.py +0 -0
  254. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_parse_conversation_base.py +0 -0
  255. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_parse_conversation_chatgpt.py +0 -0
  256. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_parse_conversation_claude.py +0 -0
  257. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_persona.py +0 -0
  258. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_pipeline.py +0 -0
  259. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy.py +0 -0
  260. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy_cli.py +0 -0
  261. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy_e2e.py +0 -0
  262. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy_output.py +0 -0
  263. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy_scan.py +0 -0
  264. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_prompt_dna.py +0 -0
  265. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_public_api.py +0 -0
  266. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_recommend.py +0 -0
  267. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_schema_version.py +0 -0
  268. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_score_cli.py +0 -0
  269. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_scorer.py +0 -0
  270. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_segmenter.py +0 -0
  271. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_session_type.py +0 -0
  272. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_share_e2e.py +0 -0
  273. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_sharing_client.py +0 -0
  274. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_source_filter.py +0 -0
  275. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_style.py +0 -0
  276. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_style_trends.py +0 -0
  277. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_cli.py +0 -0
  278. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_collector.py +0 -0
  279. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_consent.py +0 -0
  280. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_e2e.py +0 -0
  281. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_events.py +0 -0
  282. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_prompt.py +0 -0
  283. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_queue.py +0 -0
  284. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_sender.py +0 -0
  285. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_template_cli.py +0 -0
  286. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_templates.py +0 -0
  287. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_timeutil.py +0 -0
  288. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_trends.py +0 -0
  289. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_trends_cli.py +0 -0
  290. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_use_cli.py +0 -0
  291. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped.py +0 -0
  292. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped_cli.py +0 -0
  293. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped_e2e.py +0 -0
  294. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped_html.py +0 -0
  295. {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped_output.py +0 -0
  296. {reprompt_cli-1.7.1 → 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.7.1
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
@@ -185,7 +185,7 @@ reprompt install-hook # adds post-session hook to Claude Code
185
185
 
186
186
  Capture prompts from ChatGPT, Claude.ai, and Gemini directly in your browser:
187
187
 
188
- 1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
188
+ 1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn) or [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
189
189
  2. **Connect to the CLI:** `reprompt install-extension`
190
190
  3. **Verify:** `reprompt extension-status`
191
191
 
@@ -236,6 +236,7 @@ reprompt lint --json # machine-readable output
236
236
 
237
237
  - **Website:** [getreprompt.dev](https://getreprompt.dev)
238
238
  - **Chrome Extension:** [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
239
+ - **Firefox Add-on:** [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
239
240
  - **PyPI:** [reprompt-cli](https://pypi.org/project/reprompt-cli/)
240
241
  - **Changelog:** [CHANGELOG.md](CHANGELOG.md)
241
242
  - **Privacy:** [getreprompt.dev/privacy](https://getreprompt.dev/privacy)
@@ -140,7 +140,7 @@ reprompt install-hook # adds post-session hook to Claude Code
140
140
 
141
141
  Capture prompts from ChatGPT, Claude.ai, and Gemini directly in your browser:
142
142
 
143
- 1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
143
+ 1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn) or [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
144
144
  2. **Connect to the CLI:** `reprompt install-extension`
145
145
  3. **Verify:** `reprompt extension-status`
146
146
 
@@ -191,6 +191,7 @@ reprompt lint --json # machine-readable output
191
191
 
192
192
  - **Website:** [getreprompt.dev](https://getreprompt.dev)
193
193
  - **Chrome Extension:** [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
194
+ - **Firefox Add-on:** [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
194
195
  - **PyPI:** [reprompt-cli](https://pypi.org/project/reprompt-cli/)
195
196
  - **Changelog:** [CHANGELOG.md](CHANGELOG.md)
196
197
  - **Privacy:** [getreprompt.dev/privacy](https://getreprompt.dev/privacy)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reprompt-cli"
3
- version = "1.7.1"
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.7.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
 
@@ -1856,7 +1869,7 @@ def _create_host_wrapper() -> Path:
1856
1869
  # Find the Python executable that has reprompt installed
1857
1870
  python_path = sys_mod.executable
1858
1871
 
1859
- wrapper_path.write_text(f"#!/bin/sh\nexec {python_path} -u -m reprompt.bridge.host\n")
1872
+ wrapper_path.write_text(f'#!/bin/sh\nexec "{python_path}" -u -m reprompt.bridge.host\n')
1860
1873
  wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC)
1861
1874
  return wrapper_path
1862
1875
 
@@ -1916,6 +1929,102 @@ def extension_status() -> None:
1916
1929
  console.print(" Last sync: never")
1917
1930
 
1918
1931
 
1932
+ @app.command(rich_help_panel="Analyze")
1933
+ def sessions(
1934
+ last: int = typer.Option(10, "--last", help="Show N most recent sessions"),
1935
+ source: str = typer.Option(
1936
+ None, "--source", "-s", help="Filter by source (e.g. claude-code, cursor)"
1937
+ ),
1938
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
1939
+ detail: str = typer.Option(None, "--detail", help="Deep-dive into a session ID"),
1940
+ copy: bool = typer.Option(False, "--copy", help="Copy result to clipboard"),
1941
+ ) -> None:
1942
+ """Session quality overview: composite scores, frustration signals, trends."""
1943
+ import json as json_mod
1944
+
1945
+ from reprompt.config import Settings
1946
+ from reprompt.output.sessions_terminal import render_session_detail, render_sessions_table
1947
+ from reprompt.storage.db import PromptDB
1948
+
1949
+ settings = Settings()
1950
+ db = PromptDB(settings.db_path)
1951
+
1952
+ if detail:
1953
+ # Single session detail view
1954
+ all_sessions = db.get_sessions_with_quality(limit=500)
1955
+ match = next((s for s in all_sessions if s.get("session_id") == detail), None)
1956
+ if not match:
1957
+ # Try prefix match
1958
+ match = next(
1959
+ (s for s in all_sessions if (s.get("session_id") or "").startswith(detail)),
1960
+ None,
1961
+ )
1962
+ if not match:
1963
+ typer.echo(f"Session '{detail}' not found.")
1964
+ raise typer.Exit(1)
1965
+ if json_output:
1966
+ typer.echo(json_mod.dumps(match, indent=2, default=str))
1967
+ else:
1968
+ typer.echo(render_session_detail(match), nl=False)
1969
+ else:
1970
+ data = db.get_sessions_with_quality(limit=last, source=source)
1971
+ if json_output:
1972
+ typer.echo(json_mod.dumps(data, indent=2, default=str))
1973
+ else:
1974
+ typer.echo(render_sessions_table(data), nl=False)
1975
+
1976
+ from reprompt.core.suggestions import get_suggestion
1977
+
1978
+ hint = get_suggestion("sessions")
1979
+ if hint:
1980
+ console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
1981
+
1982
+ if copy:
1983
+ if detail:
1984
+ copy_text = json_mod.dumps(match, indent=2, default=str) # type: ignore[possibly-undefined]
1985
+ else:
1986
+ copy_text = json_mod.dumps(data, indent=2, default=str) # type: ignore[possibly-undefined]
1987
+ _copy_to_clip(copy_text, quiet=json_output)
1988
+
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
+
1919
2028
  @app.command(rich_help_panel="Analyze")
1920
2029
  def agent(
1921
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
+ }
@@ -185,6 +185,105 @@ def run_scan(
185
185
  except Exception:
186
186
  logger.debug("Session metadata extraction failed for %s", file_path, exc_info=True)
187
187
 
188
+ # Compute session quality scores
189
+ from datetime import datetime
190
+
191
+ from reprompt.core.agent import analyze_session
192
+ from reprompt.core.conversation import Conversation
193
+ from reprompt.core.distill import distill_conversation
194
+ from reprompt.core.session_quality import score_session
195
+
196
+ quality_failures = 0
197
+ for file_path, adapter_name in scanned_files:
198
+ try:
199
+ matched = next((a for a in adapters if a.name == adapter_name), None)
200
+ if not matched:
201
+ continue
202
+
203
+ # parse_conversation returns list[ConversationTurn], wrap into Conversation
204
+ turns = matched.parse_conversation(Path(file_path))
205
+ if not turns:
206
+ continue
207
+
208
+ session_id = Path(file_path).stem
209
+ start_time = None
210
+ end_time = None
211
+ duration = None
212
+ timestamps = [t.timestamp for t in turns if t.timestamp]
213
+ if len(timestamps) >= 2:
214
+ start_time = timestamps[0]
215
+ end_time = timestamps[-1]
216
+ try:
217
+ start_dt = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
218
+ end_dt = datetime.fromisoformat(end_time.replace("Z", "+00:00"))
219
+ duration = int((end_dt - start_dt).total_seconds())
220
+ except (ValueError, TypeError):
221
+ pass
222
+
223
+ project = None
224
+ if hasattr(matched, "_project_from_path"):
225
+ project = matched._project_from_path(file_path)
226
+
227
+ conversation = Conversation(
228
+ session_id=session_id,
229
+ source=adapter_name,
230
+ project=project,
231
+ turns=turns,
232
+ start_time=start_time,
233
+ end_time=end_time,
234
+ duration_seconds=duration,
235
+ )
236
+
237
+ # Agent analysis (efficiency, error loops)
238
+ agent_report = None
239
+ try:
240
+ agent_report = analyze_session(conversation)
241
+ except Exception:
242
+ logger.warning("Agent analysis failed for %s", file_path, exc_info=True)
243
+
244
+ # Distill analysis (focus/retention)
245
+ distill_result = None
246
+ try:
247
+ distill_result = distill_conversation(conversation)
248
+ except Exception:
249
+ logger.warning("Distill analysis failed for %s", file_path, exc_info=True)
250
+
251
+ # Avg prompt score from stored features
252
+ avg_prompt_score = None
253
+ scores = db.get_prompt_scores_for_session(session_id)
254
+ if scores:
255
+ avg_prompt_score = sum(scores) / len(scores)
256
+
257
+ # Effectiveness score from session_meta
258
+ effectiveness = db.get_effectiveness_for_session(session_id)
259
+
260
+ quality = score_session(
261
+ conversation,
262
+ agent_report=agent_report,
263
+ distill_result=distill_result,
264
+ effectiveness_score=effectiveness,
265
+ avg_prompt_score=avg_prompt_score,
266
+ )
267
+ db.upsert_session_quality(
268
+ session_id=quality.session_id,
269
+ quality_score=quality.quality_score,
270
+ prompt_quality_score=quality.prompt_quality,
271
+ efficiency_score=quality.efficiency,
272
+ focus_score=quality.focus,
273
+ outcome_score=quality.outcome,
274
+ has_abandonment=quality.frustration.abandonment,
275
+ has_escalation=quality.frustration.escalation,
276
+ stall_turns=quality.frustration.stall_turns,
277
+ session_type=quality.session_type,
278
+ quality_insight=quality.insight,
279
+ )
280
+ except Exception:
281
+ quality_failures += 1
282
+ logger.warning("Session quality scoring failed for %s", file_path, exc_info=True)
283
+
284
+ if quality_failures:
285
+ logger.warning("Quality scoring failed for %d session(s)", quality_failures)
286
+
188
287
  # Mark sessions processed only after successful dedup+store
189
288
  for file_path, adapter_name in scanned_files:
190
289
  db.mark_session_processed(file_path, source=adapter_name)
@@ -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
+ )
@@ -0,0 +1,201 @@
1
+ """Session-level quality metrics — composite scoring + frustration detection.
2
+
3
+ Combines prompt quality, agent efficiency, distill focus, and outcome
4
+ into a single 0-100 session score. All analysis is rule-based (zero LLM, <1ms).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+
11
+ from reprompt.core.agent import AgentReport
12
+ from reprompt.core.conversation import Conversation, ConversationTurn, DistillResult
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Component weights (must sum to 1.0)
16
+ # ---------------------------------------------------------------------------
17
+
18
+ DEFAULT_WEIGHTS: dict[str, float] = {
19
+ "prompt_quality": 0.30,
20
+ "efficiency": 0.30,
21
+ "focus": 0.20,
22
+ "outcome": 0.20,
23
+ }
24
+
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Dataclasses
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ @dataclass
32
+ class FrustrationSignals:
33
+ """Frustration indicators detected from conversation turns."""
34
+
35
+ abandonment: bool = False # Last 3+ assistant turns all have errors
36
+ escalation: bool = False # Error rate in second half > first half * 1.5
37
+ stall_turns: int = 0 # Assistant turns with 0 tool calls and <50 chars
38
+
39
+
40
+ @dataclass
41
+ class SessionQuality:
42
+ """Composite session quality report."""
43
+
44
+ session_id: str
45
+ quality_score: float # 0-100 composite
46
+ prompt_quality: float | None = None # 0-100
47
+ efficiency: float | None = None # 0-100
48
+ focus: float | None = None # 0-100
49
+ outcome: float | None = None # 0-100
50
+ frustration: FrustrationSignals = field(default_factory=FrustrationSignals)
51
+ session_type: str | None = None
52
+ insight: str = ""
53
+ components_available: int = 0
54
+
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Frustration detection
58
+ # ---------------------------------------------------------------------------
59
+
60
+
61
+ def _detect_frustration(turns: list[ConversationTurn]) -> FrustrationSignals:
62
+ """Detect frustration signals from conversation turns."""
63
+ asst_turns = [t for t in turns if t.role == "assistant"]
64
+
65
+ if not asst_turns:
66
+ return FrustrationSignals()
67
+
68
+ # Abandonment: last 3+ assistant turns all have errors
69
+ abandonment = False
70
+ if len(asst_turns) >= 3:
71
+ last_three = asst_turns[-3:]
72
+ abandonment = all(t.has_error for t in last_three)
73
+
74
+ # Escalation: error rate in second half > first half * 1.5
75
+ escalation = False
76
+ if len(asst_turns) >= 4:
77
+ mid = len(asst_turns) // 2
78
+ first_half = asst_turns[:mid]
79
+ second_half = asst_turns[mid:]
80
+ first_rate = sum(1 for t in first_half if t.has_error) / len(first_half)
81
+ second_rate = sum(1 for t in second_half if t.has_error) / len(second_half)
82
+ escalation = second_rate > first_rate * 1.5 and second_rate > 0.2
83
+
84
+ # Stall turns: assistant turns with no tool calls and short text
85
+ stall_turns = sum(1 for t in asst_turns if t.tool_calls == 0 and len(t.text.strip()) < 50)
86
+
87
+ return FrustrationSignals(
88
+ abandonment=abandonment,
89
+ escalation=escalation,
90
+ stall_turns=stall_turns,
91
+ )
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # Insight generation
96
+ # ---------------------------------------------------------------------------
97
+
98
+
99
+ def _generate_insight(quality: SessionQuality) -> str:
100
+ """Generate a one-line insight from quality metrics (priority order)."""
101
+ f = quality.frustration
102
+
103
+ if f.abandonment:
104
+ return "Ended with unresolved errors"
105
+
106
+ if f.escalation:
107
+ return "Errors escalated through session"
108
+
109
+ if f.stall_turns >= 5:
110
+ return f"{f.stall_turns} stall turns detected"
111
+
112
+ if quality.efficiency is not None and quality.efficiency < 50:
113
+ return "Low efficiency (error loops)"
114
+
115
+ score = quality.quality_score
116
+ if score >= 80:
117
+ return "Focused session"
118
+ if score >= 60:
119
+ return "Solid session"
120
+ if score >= 40:
121
+ return "Room for improvement"
122
+ return "Rough session"
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Main scoring function
127
+ # ---------------------------------------------------------------------------
128
+
129
+
130
+ def score_session(
131
+ conversation: Conversation,
132
+ *,
133
+ agent_report: AgentReport | None = None,
134
+ distill_result: DistillResult | None = None,
135
+ effectiveness_score: float | None = None,
136
+ avg_prompt_score: float | None = None,
137
+ ) -> SessionQuality:
138
+ """Compute composite session quality score (0-100).
139
+
140
+ Components (weighted average of available inputs):
141
+ - prompt_quality (30%): avg overall_score from prompt features
142
+ - efficiency (30%): productive_ratio from agent analysis
143
+ - focus (20%): retention_ratio from distill
144
+ - outcome (20%): effectiveness_score
145
+
146
+ When a component is unavailable, its weight redistributes proportionally.
147
+ """
148
+ components: dict[str, float] = {}
149
+
150
+ # Prompt quality: already 0-100
151
+ if avg_prompt_score is not None:
152
+ components["prompt_quality"] = max(0.0, min(100.0, avg_prompt_score))
153
+
154
+ # Efficiency: productive_ratio is 0-1
155
+ if agent_report is not None:
156
+ ratio = agent_report.efficiency.productive_ratio
157
+ components["efficiency"] = max(0.0, min(100.0, ratio * 100))
158
+
159
+ # Focus: retention_ratio is 0-1
160
+ if distill_result is not None:
161
+ ratio = distill_result.stats.retention_ratio
162
+ components["focus"] = max(0.0, min(100.0, ratio * 100))
163
+
164
+ # Outcome: effectiveness_score is 0-1
165
+ if effectiveness_score is not None:
166
+ components["outcome"] = max(0.0, min(100.0, effectiveness_score * 100))
167
+
168
+ # Compute weighted average with weight redistribution
169
+ if components:
170
+ available_weights = {k: DEFAULT_WEIGHTS[k] for k in components}
171
+ weight_sum = sum(available_weights.values())
172
+ normalized = {k: w / weight_sum for k, w in available_weights.items()}
173
+ quality_score = sum(components[k] * normalized[k] for k in components)
174
+ else:
175
+ quality_score = 0.0
176
+
177
+ quality_score = round(max(0.0, min(100.0, quality_score)), 1)
178
+
179
+ # Frustration detection
180
+ frustration = _detect_frustration(conversation.turns)
181
+
182
+ # Session type
183
+ session_type_str: str | None = None
184
+ if agent_report is not None and agent_report.efficiency.session_type is not None:
185
+ session_type_str = agent_report.efficiency.session_type
186
+
187
+ quality = SessionQuality(
188
+ session_id=conversation.session_id,
189
+ quality_score=quality_score,
190
+ prompt_quality=components.get("prompt_quality"),
191
+ efficiency=components.get("efficiency"),
192
+ focus=components.get("focus"),
193
+ outcome=components.get("outcome"),
194
+ frustration=frustration,
195
+ session_type=session_type_str,
196
+ components_available=len(components),
197
+ )
198
+
199
+ quality.insight = _generate_insight(quality)
200
+
201
+ return quality