reprompt-cli 1.7.1__tar.gz → 1.8.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 (291) hide show
  1. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/PKG-INFO +3 -2
  2. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/README.md +2 -1
  3. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/pyproject.toml +1 -1
  4. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/__init__.py +1 -1
  5. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/cli.py +59 -1
  6. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/pipeline.py +99 -0
  7. reprompt_cli-1.8.0/src/reprompt/core/session_quality.py +203 -0
  8. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/suggestions.py +3 -0
  9. reprompt_cli-1.8.0/src/reprompt/output/sessions_terminal.py +172 -0
  10. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/storage/db.py +123 -1
  11. reprompt_cli-1.8.0/tests/test_db_session_quality.py +181 -0
  12. reprompt_cli-1.8.0/tests/test_session_quality.py +455 -0
  13. reprompt_cli-1.8.0/tests/test_sessions_cli.py +193 -0
  14. reprompt_cli-1.8.0/tests/test_sessions_output.py +102 -0
  15. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_suggestions.py +5 -1
  16. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/uv.lock +1 -1
  17. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.editorconfig +0 -0
  18. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  20. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  21. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  22. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  23. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/dependabot.yml +0 -0
  24. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/workflows/ci.yml +0 -0
  25. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.github/workflows/publish.yml +0 -0
  26. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.gitignore +0 -0
  27. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.pre-commit-config.yaml +0 -0
  28. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.pre-commit-hooks.yaml +0 -0
  29. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.testmondata-shm +0 -0
  30. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/.testmondata-wal +0 -0
  31. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/CHANGELOG.md +0 -0
  32. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/CODE_OF_CONDUCT.md +0 -0
  33. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/CONTRIBUTING.md +0 -0
  34. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/LICENSE +0 -0
  35. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/SECURITY.md +0 -0
  36. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
  37. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/action.yml +0 -0
  38. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/demo.gif +0 -0
  39. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-128.png +0 -0
  40. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-16.png +0 -0
  41. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-256.png +0 -0
  42. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-32.png +0 -0
  43. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-48.png +0 -0
  44. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-512.png +0 -0
  45. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon-96.png +0 -0
  46. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/brand-icon.svg +0 -0
  47. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-128.png +0 -0
  48. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-16.png +0 -0
  49. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-256.png +0 -0
  50. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-32.png +0 -0
  51. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-48.png +0 -0
  52. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-512.png +0 -0
  53. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon-96.png +0 -0
  54. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-favicon.svg +0 -0
  55. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-128.png +0 -0
  56. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-16.png +0 -0
  57. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-256.png +0 -0
  58. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-32.png +0 -0
  59. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-48.png +0 -0
  60. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-512.png +0 -0
  61. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon-96.png +0 -0
  62. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/cli-icon.svg +0 -0
  63. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-128.png +0 -0
  64. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-16.png +0 -0
  65. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-256.png +0 -0
  66. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-32.png +0 -0
  67. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-48.png +0 -0
  68. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-512.png +0 -0
  69. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon-96.png +0 -0
  70. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/favicon.svg +0 -0
  71. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/icons/generate.sh +0 -0
  72. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
  73. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
  74. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/scripts/generate_demo_data.py +0 -0
  75. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/__init__.py +0 -0
  76. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/aider.py +0 -0
  77. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/base.py +0 -0
  78. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/chatgpt.py +0 -0
  79. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/claude_chat.py +0 -0
  80. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/claude_code.py +0 -0
  81. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/cline.py +0 -0
  82. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/codex.py +0 -0
  83. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/cursor.py +0 -0
  84. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/filters.py +0 -0
  85. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/gemini.py +0 -0
  86. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/adapters/openclaw.py +0 -0
  87. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/__init__.py +0 -0
  88. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/handler.py +0 -0
  89. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/host.py +0 -0
  90. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/manifest.py +0 -0
  91. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/bridge/protocol.py +0 -0
  92. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/commands/__init__.py +0 -0
  93. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/commands/telemetry.py +0 -0
  94. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/commands/wrapped.py +0 -0
  95. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/config.py +0 -0
  96. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/__init__.py +0 -0
  97. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/agent.py +0 -0
  98. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/analyzer.py +0 -0
  99. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/compress.py +0 -0
  100. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/conversation.py +0 -0
  101. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/cost.py +0 -0
  102. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/dashboard.py +0 -0
  103. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/dedup.py +0 -0
  104. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/digest.py +0 -0
  105. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/distill.py +0 -0
  106. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/effectiveness.py +0 -0
  107. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/extractors.py +0 -0
  108. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/extractors_zh.py +0 -0
  109. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/insights.py +0 -0
  110. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/lang_detect.py +0 -0
  111. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/library.py +0 -0
  112. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/lint.py +0 -0
  113. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/merge_view.py +0 -0
  114. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/models.py +0 -0
  115. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/persona.py +0 -0
  116. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/privacy.py +0 -0
  117. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/privacy_scan.py +0 -0
  118. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/prompt_dna.py +0 -0
  119. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/recommend.py +0 -0
  120. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/scorer.py +0 -0
  121. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/segmenter.py +0 -0
  122. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/session_meta.py +0 -0
  123. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/session_type.py +0 -0
  124. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/style.py +0 -0
  125. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/templates.py +0 -0
  126. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/timeutil.py +0 -0
  127. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/trends.py +0 -0
  128. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/core/wrapped.py +0 -0
  129. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/demo.py +0 -0
  130. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/__init__.py +0 -0
  131. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/base.py +0 -0
  132. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/local_embed.py +0 -0
  133. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/ollama.py +0 -0
  134. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/openai_embed.py +0 -0
  135. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/embeddings/tfidf.py +0 -0
  136. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/mcp.py +0 -0
  137. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/mcp_main.py +0 -0
  138. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/__init__.py +0 -0
  139. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/agent_terminal.py +0 -0
  140. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/chartjs.min.js +0 -0
  141. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/compress_terminal.py +0 -0
  142. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/dashboard_terminal.py +0 -0
  143. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/distill_terminal.py +0 -0
  144. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/export.py +0 -0
  145. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/html_report.py +0 -0
  146. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/json_out.py +0 -0
  147. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/markdown.py +0 -0
  148. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/terminal.py +0 -0
  149. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/wrapped_html.py +0 -0
  150. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/output/wrapped_terminal.py +0 -0
  151. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/py.typed +0 -0
  152. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/sharing/__init__.py +0 -0
  153. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/sharing/client.py +0 -0
  154. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/sharing/clipboard.py +0 -0
  155. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/storage/__init__.py +0 -0
  156. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/__init__.py +0 -0
  157. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/collector.py +0 -0
  158. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/consent.py +0 -0
  159. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/events.py +0 -0
  160. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/prompt.py +0 -0
  161. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/queue.py +0 -0
  162. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/src/reprompt/telemetry/sender.py +0 -0
  163. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/__init__.py +0 -0
  164. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/conftest.py +0 -0
  165. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/aider_chat_history.md +0 -0
  166. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/chatgpt_conversations.json +0 -0
  167. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/claude_chat_export.json +0 -0
  168. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/claude_session.jsonl +0 -0
  169. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
  170. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/export/default_export.md +0 -0
  171. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/export/full_export.md +0 -0
  172. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/gemini_session.json +0 -0
  173. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/fixtures/openclaw_session.jsonl +0 -0
  174. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_aider.py +0 -0
  175. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_chatgpt.py +0 -0
  176. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_claude.py +0 -0
  177. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_claude_chat.py +0 -0
  178. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_cline.py +0 -0
  179. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_gemini.py +0 -0
  180. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_adapter_openclaw.py +0 -0
  181. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_agent.py +0 -0
  182. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_agent_cli.py +0 -0
  183. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_analyzer.py +0 -0
  184. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_cli.py +0 -0
  185. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_e2e.py +0 -0
  186. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_handler.py +0 -0
  187. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_integration.py +0 -0
  188. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_manifest.py +0 -0
  189. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_bridge_protocol.py +0 -0
  190. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cli.py +0 -0
  191. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cli_deprecations.py +0 -0
  192. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cli_library_effectiveness.py +0 -0
  193. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_clipboard.py +0 -0
  194. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_codex_adapter.py +0 -0
  195. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compare_best_worst.py +0 -0
  196. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress.py +0 -0
  197. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress_cli.py +0 -0
  198. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress_dna.py +0 -0
  199. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress_html.py +0 -0
  200. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_compress_insights.py +0 -0
  201. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_config.py +0 -0
  202. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_conversation.py +0 -0
  203. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_copy_flag.py +0 -0
  204. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cost.py +0 -0
  205. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_coverage_boost.py +0 -0
  206. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_cursor_adapter.py +0 -0
  207. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_dashboard.py +0 -0
  208. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_db.py +0 -0
  209. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_db_digest.py +0 -0
  210. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_db_effectiveness.py +0 -0
  211. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_db_trends.py +0 -0
  212. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_dedup.py +0 -0
  213. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_demo.py +0 -0
  214. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_deprecated_commands.py +0 -0
  215. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_digest.py +0 -0
  216. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_digest_cli.py +0 -0
  217. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_distill.py +0 -0
  218. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_distill_cli.py +0 -0
  219. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_distill_weights.py +0 -0
  220. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_e2e.py +0 -0
  221. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_effectiveness.py +0 -0
  222. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_embeddings_local.py +0 -0
  223. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_embeddings_ollama.py +0 -0
  224. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_embeddings_openai.py +0 -0
  225. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_empty_state.py +0 -0
  226. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_export.py +0 -0
  227. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_export_cli.py +0 -0
  228. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_export_snapshot.py +0 -0
  229. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_extractors.py +0 -0
  230. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_extractors_routing.py +0 -0
  231. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_extractors_zh.py +0 -0
  232. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_extractors_zh_e2e.py +0 -0
  233. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_html_report.py +0 -0
  234. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_import_cli.py +0 -0
  235. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_import_e2e.py +0 -0
  236. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_insights.py +0 -0
  237. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_insights_cli.py +0 -0
  238. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_insights_expanded.py +0 -0
  239. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_install_hook.py +0 -0
  240. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_lang_detect.py +0 -0
  241. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_library.py +0 -0
  242. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_lint.py +0 -0
  243. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_lint_cli.py +0 -0
  244. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_markdown.py +0 -0
  245. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_mcp.py +0 -0
  246. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_merge_view.py +0 -0
  247. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_models.py +0 -0
  248. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_output.py +0 -0
  249. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_parse_conversation_base.py +0 -0
  250. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_parse_conversation_chatgpt.py +0 -0
  251. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_parse_conversation_claude.py +0 -0
  252. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_persona.py +0 -0
  253. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_pipeline.py +0 -0
  254. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy.py +0 -0
  255. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy_cli.py +0 -0
  256. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy_e2e.py +0 -0
  257. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy_output.py +0 -0
  258. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_privacy_scan.py +0 -0
  259. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_prompt_dna.py +0 -0
  260. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_public_api.py +0 -0
  261. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_recommend.py +0 -0
  262. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_schema_version.py +0 -0
  263. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_score_cli.py +0 -0
  264. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_scorer.py +0 -0
  265. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_segmenter.py +0 -0
  266. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_session_type.py +0 -0
  267. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_share_e2e.py +0 -0
  268. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_sharing_client.py +0 -0
  269. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_source_filter.py +0 -0
  270. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_style.py +0 -0
  271. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_style_trends.py +0 -0
  272. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_cli.py +0 -0
  273. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_collector.py +0 -0
  274. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_consent.py +0 -0
  275. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_e2e.py +0 -0
  276. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_events.py +0 -0
  277. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_prompt.py +0 -0
  278. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_queue.py +0 -0
  279. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_telemetry_sender.py +0 -0
  280. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_template_cli.py +0 -0
  281. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_templates.py +0 -0
  282. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_timeutil.py +0 -0
  283. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_trends.py +0 -0
  284. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_trends_cli.py +0 -0
  285. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_use_cli.py +0 -0
  286. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped.py +0 -0
  287. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped_cli.py +0 -0
  288. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped_e2e.py +0 -0
  289. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped_html.py +0 -0
  290. {reprompt_cli-1.7.1 → reprompt_cli-1.8.0}/tests/test_wrapped_output.py +0 -0
  291. {reprompt_cli-1.7.1 → reprompt_cli-1.8.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.7.1
3
+ Version: 1.8.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
@@ -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.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.7.0"
3
+ __version__ = "1.8.0"
4
4
 
5
5
  __all__ = [
6
6
  "__version__",
@@ -1856,7 +1856,7 @@ def _create_host_wrapper() -> Path:
1856
1856
  # Find the Python executable that has reprompt installed
1857
1857
  python_path = sys_mod.executable
1858
1858
 
1859
- wrapper_path.write_text(f"#!/bin/sh\nexec {python_path} -u -m reprompt.bridge.host\n")
1859
+ wrapper_path.write_text(f'#!/bin/sh\nexec "{python_path}" -u -m reprompt.bridge.host\n')
1860
1860
  wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC)
1861
1861
  return wrapper_path
1862
1862
 
@@ -1916,6 +1916,64 @@ def extension_status() -> None:
1916
1916
  console.print(" Last sync: never")
1917
1917
 
1918
1918
 
1919
+ @app.command(rich_help_panel="Analyze")
1920
+ def sessions(
1921
+ last: int = typer.Option(10, "--last", help="Show N most recent sessions"),
1922
+ source: str = typer.Option(
1923
+ None, "--source", "-s", help="Filter by source (e.g. claude-code, cursor)"
1924
+ ),
1925
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
1926
+ detail: str = typer.Option(None, "--detail", help="Deep-dive into a session ID"),
1927
+ copy: bool = typer.Option(False, "--copy", help="Copy result to clipboard"),
1928
+ ) -> None:
1929
+ """Session quality overview: composite scores, frustration signals, trends."""
1930
+ import json as json_mod
1931
+
1932
+ from reprompt.config import Settings
1933
+ from reprompt.output.sessions_terminal import render_session_detail, render_sessions_table
1934
+ from reprompt.storage.db import PromptDB
1935
+
1936
+ settings = Settings()
1937
+ db = PromptDB(settings.db_path)
1938
+
1939
+ if detail:
1940
+ # Single session detail view
1941
+ all_sessions = db.get_sessions_with_quality(limit=500)
1942
+ match = next((s for s in all_sessions if s.get("session_id") == detail), None)
1943
+ if not match:
1944
+ # Try prefix match
1945
+ match = next(
1946
+ (s for s in all_sessions if (s.get("session_id") or "").startswith(detail)),
1947
+ None,
1948
+ )
1949
+ if not match:
1950
+ typer.echo(f"Session '{detail}' not found.")
1951
+ raise typer.Exit(1)
1952
+ if json_output:
1953
+ typer.echo(json_mod.dumps(match, indent=2, default=str))
1954
+ else:
1955
+ typer.echo(render_session_detail(match), nl=False)
1956
+ else:
1957
+ data = db.get_sessions_with_quality(limit=last, source=source)
1958
+ if json_output:
1959
+ typer.echo(json_mod.dumps(data, indent=2, default=str))
1960
+ else:
1961
+ typer.echo(render_sessions_table(data), nl=False)
1962
+
1963
+ from reprompt.core.suggestions import get_suggestion
1964
+
1965
+ hint = get_suggestion("sessions")
1966
+ if hint:
1967
+ console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
1968
+
1969
+ if copy:
1970
+ if detail:
1971
+ copy_text = json_mod.dumps(match, indent=2, default=str) # type: ignore[possibly-undefined]
1972
+ else:
1973
+ copy_text = json_mod.dumps(data, indent=2, default=str) # type: ignore[possibly-undefined]
1974
+ _copy_to_clip(copy_text, quiet=json_output)
1975
+
1976
+
1919
1977
  @app.command(rich_help_panel="Analyze")
1920
1978
  def agent(
1921
1979
  last: int = typer.Option(5, "--last", help="Analyze N most recent sessions"),
@@ -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,203 @@
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(
86
+ 1 for t in asst_turns if t.tool_calls == 0 and len(t.text.strip()) < 50
87
+ )
88
+
89
+ return FrustrationSignals(
90
+ abandonment=abandonment,
91
+ escalation=escalation,
92
+ stall_turns=stall_turns,
93
+ )
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Insight generation
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ def _generate_insight(quality: SessionQuality) -> str:
102
+ """Generate a one-line insight from quality metrics (priority order)."""
103
+ f = quality.frustration
104
+
105
+ if f.abandonment:
106
+ return "Ended with unresolved errors"
107
+
108
+ if f.escalation:
109
+ return "Errors escalated through session"
110
+
111
+ if f.stall_turns >= 5:
112
+ return f"{f.stall_turns} stall turns detected"
113
+
114
+ if quality.efficiency is not None and quality.efficiency < 50:
115
+ return "Low efficiency (error loops)"
116
+
117
+ score = quality.quality_score
118
+ if score >= 80:
119
+ return "Focused session"
120
+ if score >= 60:
121
+ return "Solid session"
122
+ if score >= 40:
123
+ return "Room for improvement"
124
+ return "Rough session"
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Main scoring function
129
+ # ---------------------------------------------------------------------------
130
+
131
+
132
+ def score_session(
133
+ conversation: Conversation,
134
+ *,
135
+ agent_report: AgentReport | None = None,
136
+ distill_result: DistillResult | None = None,
137
+ effectiveness_score: float | None = None,
138
+ avg_prompt_score: float | None = None,
139
+ ) -> SessionQuality:
140
+ """Compute composite session quality score (0-100).
141
+
142
+ Components (weighted average of available inputs):
143
+ - prompt_quality (30%): avg overall_score from prompt features
144
+ - efficiency (30%): productive_ratio from agent analysis
145
+ - focus (20%): retention_ratio from distill
146
+ - outcome (20%): effectiveness_score
147
+
148
+ When a component is unavailable, its weight redistributes proportionally.
149
+ """
150
+ components: dict[str, float] = {}
151
+
152
+ # Prompt quality: already 0-100
153
+ if avg_prompt_score is not None:
154
+ components["prompt_quality"] = max(0.0, min(100.0, avg_prompt_score))
155
+
156
+ # Efficiency: productive_ratio is 0-1
157
+ if agent_report is not None:
158
+ ratio = agent_report.efficiency.productive_ratio
159
+ components["efficiency"] = max(0.0, min(100.0, ratio * 100))
160
+
161
+ # Focus: retention_ratio is 0-1
162
+ if distill_result is not None:
163
+ ratio = distill_result.stats.retention_ratio
164
+ components["focus"] = max(0.0, min(100.0, ratio * 100))
165
+
166
+ # Outcome: effectiveness_score is 0-1
167
+ if effectiveness_score is not None:
168
+ components["outcome"] = max(0.0, min(100.0, effectiveness_score * 100))
169
+
170
+ # Compute weighted average with weight redistribution
171
+ if components:
172
+ available_weights = {k: DEFAULT_WEIGHTS[k] for k in components}
173
+ weight_sum = sum(available_weights.values())
174
+ normalized = {k: w / weight_sum for k, w in available_weights.items()}
175
+ quality_score = sum(components[k] * normalized[k] for k in components)
176
+ else:
177
+ quality_score = 0.0
178
+
179
+ quality_score = round(max(0.0, min(100.0, quality_score)), 1)
180
+
181
+ # Frustration detection
182
+ frustration = _detect_frustration(conversation.turns)
183
+
184
+ # Session type
185
+ session_type_str: str | None = None
186
+ if agent_report is not None and agent_report.efficiency.session_type is not None:
187
+ session_type_str = agent_report.efficiency.session_type
188
+
189
+ quality = SessionQuality(
190
+ session_id=conversation.session_id,
191
+ quality_score=quality_score,
192
+ prompt_quality=components.get("prompt_quality"),
193
+ efficiency=components.get("efficiency"),
194
+ focus=components.get("focus"),
195
+ outcome=components.get("outcome"),
196
+ frustration=frustration,
197
+ session_type=session_type_str,
198
+ components_available=len(components),
199
+ )
200
+
201
+ quality.insight = _generate_insight(quality)
202
+
203
+ return quality
@@ -15,6 +15,9 @@ SUGGESTIONS: dict[str, str] = {
15
15
  "agent": (
16
16
  "reprompt agent --loops-only (error loops) · reprompt privacy --deep (sensitive content)"
17
17
  ),
18
+ "sessions": (
19
+ "reprompt sessions --detail <id> (deep-dive) · reprompt agent (error loop analysis)"
20
+ ),
18
21
  "template": "reprompt insights (see which patterns work best)",
19
22
  }
20
23
 
@@ -0,0 +1,172 @@
1
+ """Rich terminal rendering for session quality metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from io import StringIO
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+
12
+
13
+ def _score_style(score: float | None) -> str:
14
+ """Return Rich style string for a quality score."""
15
+ if score is None:
16
+ return "dim"
17
+ if score >= 80:
18
+ return "green"
19
+ if score >= 60:
20
+ return "yellow"
21
+ if score >= 40:
22
+ return "dim"
23
+ return "red"
24
+
25
+
26
+ def _fmt_score(score: float | None) -> str:
27
+ if score is None:
28
+ return "—"
29
+ return f"{score:.0f}"
30
+
31
+
32
+ def _fmt_duration(seconds: int | None) -> str:
33
+ if seconds is None:
34
+ return "—"
35
+ if seconds < 60:
36
+ return f"{seconds}s"
37
+ return f"{seconds // 60}min"
38
+
39
+
40
+ def _bar(value: float, width: int = 10) -> str:
41
+ """Render a 0-100 value as a bar chart."""
42
+ filled = max(0, min(width, round(value / 100 * width)))
43
+ return "\u2588" * filled + "\u2591" * (width - filled)
44
+
45
+
46
+ def render_sessions_table(sessions: list[dict[str, Any]]) -> str:
47
+ """Render session quality overview as formatted terminal output."""
48
+ console = Console(record=True, width=100, file=StringIO())
49
+
50
+ if not sessions:
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
+ )
55
+ return console.export_text()
56
+
57
+ # Compute avg quality
58
+ scored = [s for s in sessions if s.get("quality_score") is not None]
59
+ avg_q = sum(s["quality_score"] for s in scored) / len(scored) if scored else 0
60
+
61
+ # Header
62
+ header = (
63
+ f"Sessions: {len(sessions)} | "
64
+ f"Scored: {len(scored)} | "
65
+ f"Avg Quality: {avg_q:.0f}/100"
66
+ )
67
+ console.print(Panel(header, title="Session Quality", border_style="cyan"))
68
+
69
+ # Table
70
+ table = Table(show_header=True, header_style="bold", padding=(0, 1))
71
+ table.add_column("Session", max_width=20)
72
+ table.add_column("Score", justify="right", width=5)
73
+ table.add_column("Type", width=6)
74
+ table.add_column("Turns", justify="right", width=5)
75
+ table.add_column("Errors", justify="right", width=6)
76
+ table.add_column("Duration", justify="right", width=8)
77
+ table.add_column("Insight", max_width=30)
78
+
79
+ for s in sessions:
80
+ score = s.get("quality_score")
81
+ style = _score_style(score)
82
+ sid = (s.get("session_id") or "")[:20]
83
+ stype = (s.get("session_type") or "")[:6]
84
+ turns = str(s.get("prompt_count") or "—")
85
+ errors = str(s.get("error_count") or "0")
86
+ duration = _fmt_duration(s.get("duration_seconds"))
87
+ insight = s.get("quality_insight") or ""
88
+
89
+ table.add_row(
90
+ sid,
91
+ f"[{style}]{_fmt_score(score)}[/{style}]",
92
+ stype,
93
+ turns,
94
+ errors,
95
+ duration,
96
+ insight,
97
+ )
98
+
99
+ console.print(table)
100
+ console.print()
101
+ return console.export_text()
102
+
103
+
104
+ def render_session_detail(session: dict[str, Any]) -> str:
105
+ """Render a single session's quality breakdown."""
106
+ console = Console(record=True, width=100, file=StringIO())
107
+
108
+ sid = session.get("session_id", "unknown")
109
+ score = session.get("quality_score")
110
+ style = _score_style(score)
111
+
112
+ console.print(
113
+ Panel(
114
+ f"Session: {sid} | Score: [{style}]{_fmt_score(score)}/100[/{style}]",
115
+ title="Session Detail",
116
+ border_style="cyan",
117
+ )
118
+ )
119
+
120
+ # Component scores
121
+ console.print()
122
+ console.print(" [bold]Quality Components[/bold]")
123
+ console.print(" \u2500" * 50)
124
+
125
+ components = [
126
+ ("Prompt Quality", session.get("prompt_quality_score"), "30%"),
127
+ ("Efficiency", session.get("efficiency_score"), "30%"),
128
+ ("Focus", session.get("focus_score"), "20%"),
129
+ ("Outcome", session.get("outcome_score"), "20%"),
130
+ ]
131
+
132
+ for name, val, weight in components:
133
+ if val is not None:
134
+ bar = _bar(val)
135
+ cs = _score_style(val)
136
+ console.print(f" {name:<16} {bar} [{cs}]{val:.0f}[/{cs}] (weight: {weight})")
137
+ else:
138
+ console.print(f" {name:<16} [dim]not available[/dim] (weight: {weight})")
139
+
140
+ # Frustration signals
141
+ console.print()
142
+ console.print(" [bold]Frustration Signals[/bold]")
143
+ console.print(" \u2500" * 50)
144
+
145
+ signals = []
146
+ if session.get("has_abandonment"):
147
+ signals.append("[red]Abandonment[/red] — session ended with unresolved errors")
148
+ if session.get("has_escalation"):
149
+ signals.append("[red]Escalation[/red] — errors increased through session")
150
+ stalls = session.get("stall_turns", 0)
151
+ if stalls > 0:
152
+ signals.append(f"[yellow]Stall turns[/yellow] — {stalls} turns with no tool use")
153
+
154
+ if signals:
155
+ for sig in signals:
156
+ console.print(f" \u26a0 {sig}")
157
+ else:
158
+ console.print(" [green]None detected[/green]")
159
+
160
+ # Session info
161
+ console.print()
162
+ console.print(" [bold]Session Info[/bold]")
163
+ console.print(" \u2500" * 50)
164
+ console.print(f" Source: {session.get('source', '—')}")
165
+ console.print(f" Type: {session.get('session_type') or '—'}")
166
+ console.print(f" Duration: {_fmt_duration(session.get('duration_seconds'))}")
167
+ console.print(f" Prompts: {session.get('prompt_count', '—')}")
168
+ console.print(f" Errors: {session.get('error_count', 0)}")
169
+ console.print(f" Insight: {session.get('quality_insight') or '—'}")
170
+
171
+ console.print()
172
+ return console.export_text()