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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/PKG-INFO +1 -1
  2. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/pyproject.toml +1 -1
  3. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/__init__.py +1 -1
  4. reprompt_cli-1.9.0/src/reprompt/bridge/handler.py +234 -0
  5. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/storage/db.py +17 -0
  6. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_bridge_e2e.py +19 -19
  7. reprompt_cli-1.9.0/tests/test_bridge_handler.py +354 -0
  8. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/uv.lock +1 -1
  9. reprompt_cli-1.8.1/src/reprompt/bridge/handler.py +0 -100
  10. reprompt_cli-1.8.1/tests/test_bridge_handler.py +0 -118
  11. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.editorconfig +0 -0
  12. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  13. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  14. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  15. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  16. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.github/dependabot.yml +0 -0
  18. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.github/workflows/ci.yml +0 -0
  19. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.github/workflows/publish.yml +0 -0
  20. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.gitignore +0 -0
  21. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.pre-commit-config.yaml +0 -0
  22. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.pre-commit-hooks.yaml +0 -0
  23. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.testmondata-shm +0 -0
  24. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/.testmondata-wal +0 -0
  25. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/CHANGELOG.md +0 -0
  26. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/CODE_OF_CONDUCT.md +0 -0
  27. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/CONTRIBUTING.md +0 -0
  28. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/LICENSE +0 -0
  29. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/README.md +0 -0
  30. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/SECURITY.md +0 -0
  31. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
  32. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/action.yml +0 -0
  33. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/demo.gif +0 -0
  34. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/brand-icon-128.png +0 -0
  35. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/brand-icon-16.png +0 -0
  36. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/brand-icon-256.png +0 -0
  37. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/brand-icon-32.png +0 -0
  38. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/brand-icon-48.png +0 -0
  39. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/brand-icon-512.png +0 -0
  40. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/brand-icon-96.png +0 -0
  41. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/brand-icon.svg +0 -0
  42. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-128.png +0 -0
  43. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-16.png +0 -0
  44. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-256.png +0 -0
  45. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-32.png +0 -0
  46. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-48.png +0 -0
  47. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-512.png +0 -0
  48. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-favicon-96.png +0 -0
  49. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-favicon.svg +0 -0
  50. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-icon-128.png +0 -0
  51. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-icon-16.png +0 -0
  52. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-icon-256.png +0 -0
  53. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-icon-32.png +0 -0
  54. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-icon-48.png +0 -0
  55. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-icon-512.png +0 -0
  56. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-icon-96.png +0 -0
  57. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/cli-icon.svg +0 -0
  58. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/favicon-128.png +0 -0
  59. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/favicon-16.png +0 -0
  60. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/favicon-256.png +0 -0
  61. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/favicon-32.png +0 -0
  62. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/favicon-48.png +0 -0
  63. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/favicon-512.png +0 -0
  64. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/favicon-96.png +0 -0
  65. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/favicon.svg +0 -0
  66. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/icons/generate.sh +0 -0
  67. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
  68. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
  69. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/scripts/generate_demo_data.py +0 -0
  70. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/__init__.py +0 -0
  71. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/aider.py +0 -0
  72. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/base.py +0 -0
  73. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/chatgpt.py +0 -0
  74. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/claude_chat.py +0 -0
  75. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/claude_code.py +0 -0
  76. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/cline.py +0 -0
  77. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/codex.py +0 -0
  78. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/cursor.py +0 -0
  79. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/filters.py +0 -0
  80. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/gemini.py +0 -0
  81. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/adapters/openclaw.py +0 -0
  82. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/bridge/__init__.py +0 -0
  83. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/bridge/host.py +0 -0
  84. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/bridge/manifest.py +0 -0
  85. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/bridge/protocol.py +0 -0
  86. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/cli.py +0 -0
  87. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/commands/__init__.py +0 -0
  88. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/commands/telemetry.py +0 -0
  89. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/commands/wrapped.py +0 -0
  90. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/config.py +0 -0
  91. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/__init__.py +0 -0
  92. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/agent.py +0 -0
  93. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/analyzer.py +0 -0
  94. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/compress.py +0 -0
  95. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/conversation.py +0 -0
  96. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/cost.py +0 -0
  97. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/dashboard.py +0 -0
  98. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/dedup.py +0 -0
  99. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/digest.py +0 -0
  100. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/distill.py +0 -0
  101. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/effectiveness.py +0 -0
  102. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/extractors.py +0 -0
  103. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/extractors_zh.py +0 -0
  104. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/insights.py +0 -0
  105. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/lang_detect.py +0 -0
  106. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/library.py +0 -0
  107. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/lint.py +0 -0
  108. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/merge_view.py +0 -0
  109. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/models.py +0 -0
  110. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/persona.py +0 -0
  111. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/pipeline.py +0 -0
  112. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/privacy.py +0 -0
  113. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/privacy_scan.py +0 -0
  114. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/prompt_dna.py +0 -0
  115. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/recommend.py +0 -0
  116. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/repetition.py +0 -0
  117. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/scorer.py +0 -0
  118. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/segmenter.py +0 -0
  119. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/session_meta.py +0 -0
  120. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/session_quality.py +0 -0
  121. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/session_type.py +0 -0
  122. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/style.py +0 -0
  123. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/suggestions.py +0 -0
  124. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/templates.py +0 -0
  125. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/timeutil.py +0 -0
  126. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/trends.py +0 -0
  127. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/core/wrapped.py +0 -0
  128. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/demo.py +0 -0
  129. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/embeddings/__init__.py +0 -0
  130. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/embeddings/base.py +0 -0
  131. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/embeddings/local_embed.py +0 -0
  132. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/embeddings/ollama.py +0 -0
  133. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/embeddings/openai_embed.py +0 -0
  134. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/embeddings/tfidf.py +0 -0
  135. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/mcp.py +0 -0
  136. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/mcp_main.py +0 -0
  137. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/__init__.py +0 -0
  138. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/agent_terminal.py +0 -0
  139. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/chartjs.min.js +0 -0
  140. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/compress_terminal.py +0 -0
  141. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/dashboard_terminal.py +0 -0
  142. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/distill_terminal.py +0 -0
  143. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/export.py +0 -0
  144. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/html_report.py +0 -0
  145. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/json_out.py +0 -0
  146. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/markdown.py +0 -0
  147. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/repetition_terminal.py +0 -0
  148. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/sessions_terminal.py +0 -0
  149. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/terminal.py +0 -0
  150. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/wrapped_html.py +0 -0
  151. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/output/wrapped_terminal.py +0 -0
  152. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/py.typed +0 -0
  153. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/sharing/__init__.py +0 -0
  154. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/sharing/client.py +0 -0
  155. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/sharing/clipboard.py +0 -0
  156. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/storage/__init__.py +0 -0
  157. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/telemetry/__init__.py +0 -0
  158. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/telemetry/collector.py +0 -0
  159. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/telemetry/consent.py +0 -0
  160. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/telemetry/events.py +0 -0
  161. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/telemetry/prompt.py +0 -0
  162. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/telemetry/queue.py +0 -0
  163. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/src/reprompt/telemetry/sender.py +0 -0
  164. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/__init__.py +0 -0
  165. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/conftest.py +0 -0
  166. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/fixtures/aider_chat_history.md +0 -0
  167. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/fixtures/chatgpt_conversations.json +0 -0
  168. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/fixtures/claude_chat_export.json +0 -0
  169. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/fixtures/claude_session.jsonl +0 -0
  170. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
  171. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/fixtures/export/default_export.md +0 -0
  172. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/fixtures/export/full_export.md +0 -0
  173. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/fixtures/gemini_session.json +0 -0
  174. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/fixtures/openclaw_session.jsonl +0 -0
  175. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_adapter_aider.py +0 -0
  176. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_adapter_chatgpt.py +0 -0
  177. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_adapter_claude.py +0 -0
  178. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_adapter_claude_chat.py +0 -0
  179. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_adapter_cline.py +0 -0
  180. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_adapter_gemini.py +0 -0
  181. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_adapter_openclaw.py +0 -0
  182. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_agent.py +0 -0
  183. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_agent_cli.py +0 -0
  184. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_analyzer.py +0 -0
  185. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_bridge_cli.py +0 -0
  186. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_bridge_integration.py +0 -0
  187. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_bridge_manifest.py +0 -0
  188. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_bridge_protocol.py +0 -0
  189. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_cli.py +0 -0
  190. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_cli_deprecations.py +0 -0
  191. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_cli_library_effectiveness.py +0 -0
  192. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_clipboard.py +0 -0
  193. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_codex_adapter.py +0 -0
  194. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_compare_best_worst.py +0 -0
  195. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_compress.py +0 -0
  196. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_compress_cli.py +0 -0
  197. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_compress_dna.py +0 -0
  198. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_compress_html.py +0 -0
  199. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_compress_insights.py +0 -0
  200. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_config.py +0 -0
  201. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_conversation.py +0 -0
  202. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_copy_flag.py +0 -0
  203. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_cost.py +0 -0
  204. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_coverage_boost.py +0 -0
  205. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_cursor_adapter.py +0 -0
  206. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_dashboard.py +0 -0
  207. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_db.py +0 -0
  208. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_db_digest.py +0 -0
  209. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_db_effectiveness.py +0 -0
  210. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_db_session_quality.py +0 -0
  211. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_db_trends.py +0 -0
  212. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_dedup.py +0 -0
  213. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_demo.py +0 -0
  214. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_deprecated_commands.py +0 -0
  215. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_digest.py +0 -0
  216. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_digest_cli.py +0 -0
  217. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_distill.py +0 -0
  218. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_distill_cli.py +0 -0
  219. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_distill_weights.py +0 -0
  220. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_e2e.py +0 -0
  221. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_effectiveness.py +0 -0
  222. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_embeddings_local.py +0 -0
  223. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_embeddings_ollama.py +0 -0
  224. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_embeddings_openai.py +0 -0
  225. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_empty_state.py +0 -0
  226. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_export.py +0 -0
  227. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_export_cli.py +0 -0
  228. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_export_snapshot.py +0 -0
  229. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_extractors.py +0 -0
  230. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_extractors_routing.py +0 -0
  231. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_extractors_zh.py +0 -0
  232. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_extractors_zh_e2e.py +0 -0
  233. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_html_report.py +0 -0
  234. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_import_cli.py +0 -0
  235. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_import_e2e.py +0 -0
  236. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_insights.py +0 -0
  237. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_insights_cli.py +0 -0
  238. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_insights_expanded.py +0 -0
  239. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_install_hook.py +0 -0
  240. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_lang_detect.py +0 -0
  241. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_library.py +0 -0
  242. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_lint.py +0 -0
  243. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_lint_cli.py +0 -0
  244. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_markdown.py +0 -0
  245. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_mcp.py +0 -0
  246. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_merge_view.py +0 -0
  247. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_models.py +0 -0
  248. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_output.py +0 -0
  249. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_parse_conversation_base.py +0 -0
  250. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_parse_conversation_chatgpt.py +0 -0
  251. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_parse_conversation_claude.py +0 -0
  252. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_persona.py +0 -0
  253. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_pipeline.py +0 -0
  254. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_privacy.py +0 -0
  255. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_privacy_cli.py +0 -0
  256. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_privacy_e2e.py +0 -0
  257. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_privacy_output.py +0 -0
  258. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_privacy_scan.py +0 -0
  259. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_prompt_dna.py +0 -0
  260. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_public_api.py +0 -0
  261. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_recommend.py +0 -0
  262. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_repetition.py +0 -0
  263. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_repetition_cli.py +0 -0
  264. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_repetition_output.py +0 -0
  265. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_schema_version.py +0 -0
  266. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_score_cli.py +0 -0
  267. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_scorer.py +0 -0
  268. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_segmenter.py +0 -0
  269. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_session_quality.py +0 -0
  270. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_session_type.py +0 -0
  271. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_sessions_cli.py +0 -0
  272. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_sessions_output.py +0 -0
  273. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_share_e2e.py +0 -0
  274. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_sharing_client.py +0 -0
  275. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_source_filter.py +0 -0
  276. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_style.py +0 -0
  277. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_style_trends.py +0 -0
  278. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_suggestions.py +0 -0
  279. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_telemetry_cli.py +0 -0
  280. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_telemetry_collector.py +0 -0
  281. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_telemetry_consent.py +0 -0
  282. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_telemetry_e2e.py +0 -0
  283. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_telemetry_events.py +0 -0
  284. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_telemetry_prompt.py +0 -0
  285. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_telemetry_queue.py +0 -0
  286. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_telemetry_sender.py +0 -0
  287. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_template_cli.py +0 -0
  288. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_templates.py +0 -0
  289. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_timeutil.py +0 -0
  290. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_trends.py +0 -0
  291. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_trends_cli.py +0 -0
  292. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_use_cli.py +0 -0
  293. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_wrapped.py +0 -0
  294. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_wrapped_cli.py +0 -0
  295. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_wrapped_e2e.py +0 -0
  296. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_wrapped_html.py +0 -0
  297. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_wrapped_output.py +0 -0
  298. {reprompt_cli-1.8.1 → reprompt_cli-1.9.0}/tests/test_wrapped_share.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reprompt-cli
3
- Version: 1.8.1
3
+ Version: 1.9.0
4
4
  Summary: Discover, analyze, and optimize your prompts from AI coding sessions
5
5
  Project-URL: Homepage, https://github.com/reprompt-dev/reprompt
6
6
  Project-URL: Repository, https://github.com/reprompt-dev/reprompt
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reprompt-cli"
3
- version = "1.8.1"
3
+ version = "1.9.0"
4
4
  description = "Discover, analyze, and optimize your prompts from AI coding sessions"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -1,6 +1,6 @@
1
1
  """reprompt — Discover, analyze, and evolve your best prompts from AI coding sessions."""
2
2
 
3
- __version__ = "1.8.1"
3
+ __version__ = "1.9.0"
4
4
 
5
5
  __all__ = [
6
6
  "__version__",
@@ -0,0 +1,234 @@
1
+ """Message handler for Native Messaging bridge.
2
+
3
+ Processes incoming messages from the browser extension:
4
+ - ping -> pong (health check)
5
+ - sync_prompts -> store in DB, return counts + lightweight insights
6
+ - get_status -> return DB stats
7
+ - get_insights -> return full analysis (repetition, patterns, top insight)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from datetime import datetime, timezone
14
+ from typing import Any
15
+
16
+ from reprompt import __version__
17
+ from reprompt.adapters.filters import should_keep_prompt
18
+ from reprompt.storage.db import PromptDB
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def handle_message(message: dict[str, Any], db: PromptDB) -> dict[str, Any]:
24
+ """Process a single message and return a response dict."""
25
+ msg_type = message.get("type", "")
26
+
27
+ if msg_type == "ping":
28
+ return {"type": "pong", "version": __version__}
29
+
30
+ if msg_type == "sync_prompts":
31
+ return _handle_sync(message, db)
32
+
33
+ if msg_type == "get_status":
34
+ return _handle_status(db)
35
+
36
+ if msg_type == "get_insights":
37
+ return _handle_insights(message, db)
38
+
39
+ return {"type": "error", "message": f"Unknown message type: {msg_type}"}
40
+
41
+
42
+ def _handle_sync(message: dict[str, Any], db: PromptDB) -> dict[str, Any]:
43
+ """Store synced prompts in DB, skipping noise and duplicates."""
44
+ prompts = message.get("prompts", [])
45
+ received = len(prompts)
46
+ new_stored = 0
47
+ duplicates = 0
48
+
49
+ for p in prompts:
50
+ text = p.get("text", "").strip()
51
+ if not should_keep_prompt(text):
52
+ continue
53
+
54
+ source = p.get("source", "extension")
55
+ session_id = p.get("conversation_id", "")
56
+ project = p.get("conversation_title", "")
57
+ timestamp = p.get("timestamp", "")
58
+
59
+ inserted = db.insert_prompt(
60
+ text,
61
+ source=source,
62
+ project=project,
63
+ session_id=session_id,
64
+ timestamp=timestamp,
65
+ )
66
+ if inserted:
67
+ new_stored += 1
68
+ else:
69
+ duplicates += 1
70
+
71
+ # Record last sync time
72
+ _update_last_sync(db)
73
+
74
+ return {
75
+ "type": "sync_result",
76
+ "received": received,
77
+ "new_stored": new_stored,
78
+ "duplicates": duplicates,
79
+ "insights": _compute_quick_insights(db),
80
+ }
81
+
82
+
83
+ def _handle_status(db: PromptDB) -> dict[str, Any]:
84
+ """Return current database stats."""
85
+ stats = db.get_stats()
86
+ return {
87
+ "type": "status",
88
+ "total_prompts": stats.get("total_prompts", 0),
89
+ "last_sync": _get_last_sync(db),
90
+ "version": __version__,
91
+ }
92
+
93
+
94
+ def _handle_insights(message: dict[str, Any], db: PromptDB) -> dict[str, Any]:
95
+ """Return full analysis: repetition, effectiveness patterns, insights.
96
+
97
+ Heavier computation than sync — call on-demand, not every sync.
98
+ """
99
+ source = message.get("source")
100
+ result: dict[str, Any] = {"type": "insights_result"}
101
+
102
+ try:
103
+ from reprompt.core.insights import (
104
+ compute_insights,
105
+ get_cross_session_repetition_insight,
106
+ get_effectiveness_insight,
107
+ )
108
+
109
+ features = db.get_all_features(source=source)
110
+ if features:
111
+ full = compute_insights(features)
112
+ result["avg_score"] = full.get("avg_score", 0.0)
113
+ result["prompt_count"] = full.get("prompt_count", 0)
114
+ result["score_distribution"] = full.get("score_distribution", {})
115
+ result["insights"] = [
116
+ {"category": i["category"], "action": i["action"], "impact": i["impact"]}
117
+ for i in full.get("insights", [])
118
+ ]
119
+ else:
120
+ result["avg_score"] = 0.0
121
+ result["prompt_count"] = 0
122
+ result["score_distribution"] = {}
123
+ result["insights"] = []
124
+
125
+ # Repetition (may be None if insufficient data)
126
+ rep = get_cross_session_repetition_insight(db, source=source)
127
+ if rep:
128
+ result["repetition"] = {
129
+ "rate": rep["repetition_rate"],
130
+ "top_topics": rep["top_topics"],
131
+ "total_recurring": rep["total_recurring_topics"],
132
+ }
133
+
134
+ # Effectiveness patterns (may be None)
135
+ eff = get_effectiveness_insight(db, source=source)
136
+ if eff:
137
+ result["effectiveness"] = {
138
+ "top_patterns": eff["top_patterns"],
139
+ "total_patterns": eff["total_patterns"],
140
+ }
141
+
142
+ except Exception:
143
+ logger.warning("Failed to compute full insights for extension", exc_info=True)
144
+ result["error"] = "Failed to compute insights"
145
+
146
+ return result
147
+
148
+
149
+ def _compute_quick_insights(db: PromptDB) -> dict[str, Any]:
150
+ """Lightweight stats for extension display. Pure SQL, no heavy computation."""
151
+ stats = db.get_stats()
152
+ total = stats.get("total_prompts", 0)
153
+
154
+ if total == 0:
155
+ return {
156
+ "avg_score": 0.0,
157
+ "total_prompts": 0,
158
+ "score_trend": "stable",
159
+ "top_insight": None,
160
+ }
161
+
162
+ scores = db.get_recent_scores(limit=50)
163
+
164
+ if not scores:
165
+ return {
166
+ "avg_score": 0.0,
167
+ "total_prompts": total,
168
+ "score_trend": "stable",
169
+ "top_insight": None,
170
+ }
171
+
172
+ avg_score = round(sum(scores) / len(scores), 1)
173
+
174
+ # Trend: compare first half (recent) vs second half (older)
175
+ mid = len(scores) // 2
176
+ if mid >= 5:
177
+ recent_avg = sum(scores[:mid]) / mid
178
+ older_avg = sum(scores[mid:]) / (len(scores) - mid)
179
+ diff = recent_avg - older_avg
180
+ if diff > 3:
181
+ trend = "improving"
182
+ elif diff < -3:
183
+ trend = "declining"
184
+ else:
185
+ trend = "stable"
186
+ else:
187
+ trend = "stable"
188
+
189
+ # Top insight: get highest-impact actionable tip
190
+ top_insight = _get_top_insight(db)
191
+
192
+ return {
193
+ "avg_score": avg_score,
194
+ "total_prompts": total,
195
+ "score_trend": trend,
196
+ "top_insight": top_insight,
197
+ }
198
+
199
+
200
+ def _get_top_insight(db: PromptDB) -> str | None:
201
+ """Return the single most impactful insight as a string, or None."""
202
+ try:
203
+ from reprompt.core.insights import compute_insights
204
+
205
+ features = db.get_all_features()
206
+ if len(features) < 5:
207
+ return None
208
+ result = compute_insights(features)
209
+ insights = result.get("insights", [])
210
+ # Prioritize high-impact insights
211
+ for impact in ("high", "medium", "low"):
212
+ for i in insights:
213
+ if i.get("impact") == impact:
214
+ return i["action"]
215
+ except Exception:
216
+ logger.debug("Failed to compute top insight", exc_info=True)
217
+ return None
218
+
219
+
220
+ def _update_last_sync(db: PromptDB) -> None:
221
+ """Store last sync timestamp in the DB settings table."""
222
+ now_ts = str(int(datetime.now(tz=timezone.utc).timestamp()))
223
+ db.set_setting("last_extension_sync", now_ts)
224
+
225
+
226
+ def _get_last_sync(db: PromptDB) -> str:
227
+ """Get last sync timestamp. Returns empty string if never synced."""
228
+ val = db.get_setting("last_extension_sync")
229
+ if val:
230
+ try:
231
+ return datetime.fromtimestamp(int(val), tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
232
+ except (ValueError, OSError):
233
+ return ""
234
+ return ""
@@ -964,6 +964,23 @@ class PromptDB:
964
964
  finally:
965
965
  conn.close()
966
966
 
967
+ def get_recent_scores(self, limit: int = 50) -> list[float]:
968
+ """Return recent prompt scores ordered by timestamp (newest first)."""
969
+ conn = self._conn()
970
+ try:
971
+ rows = conn.execute(
972
+ """SELECT pf.overall_score
973
+ FROM prompt_features pf
974
+ JOIN prompts p ON pf.prompt_hash = p.hash
975
+ WHERE pf.overall_score IS NOT NULL
976
+ ORDER BY p.timestamp DESC
977
+ LIMIT ?""",
978
+ (limit,),
979
+ ).fetchall()
980
+ return [r["overall_score"] for r in rows]
981
+ finally:
982
+ conn.close()
983
+
967
984
  def get_all_features(self, source: str | None = None) -> list[dict[str, Any]]:
968
985
  """Return all stored feature vectors, optionally filtered by source."""
969
986
  conn = self._conn()
@@ -129,25 +129,25 @@ class TestExtensionE2EPipeline:
129
129
  # Verify ping
130
130
  assert responses[0]["type"] == "pong"
131
131
 
132
- # Verify sync results
133
- assert responses[1] == {
134
- "type": "sync_result",
135
- "received": 2,
136
- "new_stored": 2,
137
- "duplicates": 0,
138
- }
139
- assert responses[2] == {
140
- "type": "sync_result",
141
- "received": 1,
142
- "new_stored": 1,
143
- "duplicates": 0,
144
- }
145
- assert responses[3] == {
146
- "type": "sync_result",
147
- "received": 1,
148
- "new_stored": 1,
149
- "duplicates": 0,
150
- }
132
+ # Verify sync results (check core fields; insights key added in v1.9)
133
+ r1 = responses[1]
134
+ assert r1["type"] == "sync_result"
135
+ assert r1["received"] == 2
136
+ assert r1["new_stored"] == 2
137
+ assert r1["duplicates"] == 0
138
+ assert "insights" in r1
139
+
140
+ r2 = responses[2]
141
+ assert r2["type"] == "sync_result"
142
+ assert r2["received"] == 1
143
+ assert r2["new_stored"] == 1
144
+ assert r2["duplicates"] == 0
145
+
146
+ r3 = responses[3]
147
+ assert r3["type"] == "sync_result"
148
+ assert r3["received"] == 1
149
+ assert r3["new_stored"] == 1
150
+ assert r3["duplicates"] == 0
151
151
 
152
152
  # Verify status shows all 4 prompts
153
153
  assert responses[4]["type"] == "status"
@@ -0,0 +1,354 @@
1
+ """Tests for Native Messaging message handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from reprompt.bridge.handler import handle_message
8
+ from reprompt.storage.db import PromptDB
9
+
10
+
11
+ def test_handle_ping(tmp_path: Path) -> None:
12
+ db = PromptDB(tmp_path / "test.db")
13
+ response = handle_message({"type": "ping"}, db)
14
+ assert response["type"] == "pong"
15
+ assert "version" in response
16
+
17
+
18
+ def test_handle_sync_prompts(tmp_path: Path) -> None:
19
+ db = PromptDB(tmp_path / "test.db")
20
+ msg = {
21
+ "type": "sync_prompts",
22
+ "prompts": [
23
+ {
24
+ "text": "Explain how async/await works in Python",
25
+ "source": "chatgpt-ext",
26
+ "timestamp": "2026-03-15T10:00:00Z",
27
+ "conversation_id": "conv-001",
28
+ "conversation_title": "Python async",
29
+ },
30
+ {
31
+ "text": "Show me a simple example of asyncio.gather",
32
+ "source": "chatgpt-ext",
33
+ "timestamp": "2026-03-15T10:01:00Z",
34
+ "conversation_id": "conv-001",
35
+ "conversation_title": "Python async",
36
+ },
37
+ ],
38
+ }
39
+ response = handle_message(msg, db)
40
+ assert response["type"] == "sync_result"
41
+ assert response["received"] == 2
42
+ assert response["new_stored"] == 2
43
+ assert response["duplicates"] == 0
44
+
45
+
46
+ def test_handle_sync_dedup(tmp_path: Path) -> None:
47
+ """Second sync of same prompts should report duplicates."""
48
+ db = PromptDB(tmp_path / "test.db")
49
+ msg = {
50
+ "type": "sync_prompts",
51
+ "prompts": [
52
+ {
53
+ "text": "Explain how async/await works in Python",
54
+ "source": "chatgpt-ext",
55
+ "timestamp": "2026-03-15T10:00:00Z",
56
+ "conversation_id": "conv-001",
57
+ "conversation_title": "Python async",
58
+ },
59
+ ],
60
+ }
61
+ handle_message(msg, db)
62
+ response = handle_message(msg, db)
63
+ assert response["new_stored"] == 0
64
+ assert response["duplicates"] == 1
65
+
66
+
67
+ def test_handle_sync_filters_short(tmp_path: Path) -> None:
68
+ """Short/noise prompts should be filtered out."""
69
+ db = PromptDB(tmp_path / "test.db")
70
+ msg = {
71
+ "type": "sync_prompts",
72
+ "prompts": [
73
+ {
74
+ "text": "ok",
75
+ "source": "chatgpt-ext",
76
+ "timestamp": "",
77
+ "conversation_id": "c1",
78
+ "conversation_title": "t",
79
+ },
80
+ {
81
+ "text": "yes",
82
+ "source": "chatgpt-ext",
83
+ "timestamp": "",
84
+ "conversation_id": "c1",
85
+ "conversation_title": "t",
86
+ },
87
+ ],
88
+ }
89
+ response = handle_message(msg, db)
90
+ assert response["received"] == 2
91
+ assert response["new_stored"] == 0
92
+
93
+
94
+ def test_handle_get_status(tmp_path: Path) -> None:
95
+ db = PromptDB(tmp_path / "test.db")
96
+ # Insert a prompt so stats are non-empty
97
+ db.insert_prompt(
98
+ "Test prompt for status check", source="chatgpt-ext", project="test", session_id="s1"
99
+ )
100
+ response = handle_message({"type": "get_status"}, db)
101
+ assert response["type"] == "status"
102
+ assert response["total_prompts"] >= 1
103
+ assert "version" in response
104
+
105
+
106
+ def test_handle_unknown_type(tmp_path: Path) -> None:
107
+ db = PromptDB(tmp_path / "test.db")
108
+ response = handle_message({"type": "unknown_xyz"}, db)
109
+ assert response["type"] == "error"
110
+ assert "unknown" in response["message"].lower()
111
+
112
+
113
+ def test_handle_empty_sync(tmp_path: Path) -> None:
114
+ db = PromptDB(tmp_path / "test.db")
115
+ response = handle_message({"type": "sync_prompts", "prompts": []}, db)
116
+ assert response["type"] == "sync_result"
117
+ assert response["received"] == 0
118
+ assert response["new_stored"] == 0
119
+
120
+
121
+ # -- Insights in sync response --
122
+
123
+
124
+ def test_sync_result_includes_insights(tmp_path: Path) -> None:
125
+ """sync_result should always include an insights dict."""
126
+ db = PromptDB(tmp_path / "test.db")
127
+ msg = {
128
+ "type": "sync_prompts",
129
+ "prompts": [
130
+ {
131
+ "text": "Explain the difference between threads and processes in Python",
132
+ "source": "chatgpt-ext",
133
+ "timestamp": "2026-03-20T10:00:00Z",
134
+ "conversation_id": "c1",
135
+ "conversation_title": "",
136
+ },
137
+ ],
138
+ }
139
+ response = handle_message(msg, db)
140
+ assert "insights" in response
141
+ insights = response["insights"]
142
+ assert "avg_score" in insights
143
+ assert "total_prompts" in insights
144
+ assert "score_trend" in insights
145
+ assert insights["score_trend"] in ("improving", "declining", "stable")
146
+
147
+
148
+ def test_sync_insights_empty_db(tmp_path: Path) -> None:
149
+ """Empty DB sync should return zero insights with stable trend."""
150
+ db = PromptDB(tmp_path / "test.db")
151
+ response = handle_message({"type": "sync_prompts", "prompts": []}, db)
152
+ insights = response["insights"]
153
+ assert insights["avg_score"] == 0.0
154
+ assert insights["total_prompts"] == 0
155
+ assert insights["score_trend"] == "stable"
156
+ assert insights["top_insight"] is None
157
+
158
+
159
+ def test_sync_insights_with_scored_prompts(tmp_path: Path) -> None:
160
+ """Insights should reflect stored scores when features exist."""
161
+ db = PromptDB(tmp_path / "test.db")
162
+ # Insert prompts and store features to simulate scored data
163
+ for i in range(10):
164
+ text = f"Implement a Python decorator that caches function results variant {i}"
165
+ db.insert_prompt(text, source="chatgpt-ext", project="test", session_id="s1")
166
+ import hashlib
167
+
168
+ h = hashlib.sha256(text.encode()).hexdigest()
169
+ db.store_features(h, {"overall_score": 60.0 + i, "task_type": "implement"})
170
+
171
+ response = handle_message({"type": "sync_prompts", "prompts": []}, db)
172
+ insights = response["insights"]
173
+ assert insights["total_prompts"] == 10
174
+ assert insights["avg_score"] > 0
175
+
176
+
177
+ def test_sync_insights_trend_improving(tmp_path: Path) -> None:
178
+ """Score trend should detect improvement."""
179
+ db = PromptDB(tmp_path / "test.db")
180
+ import hashlib
181
+
182
+ # Insert 30 prompts: older ones low score, recent ones high score
183
+ for i in range(30):
184
+ ts = f"2026-03-{10 + i // 2:02d}T{10 + i % 24:02d}:00:00Z"
185
+ text = f"Write a comprehensive test suite for the user auth module version {i}"
186
+ db.insert_prompt(text, source="chatgpt-ext", project="test", session_id="s1", timestamp=ts)
187
+ h = hashlib.sha256(text.encode()).hexdigest()
188
+ # Older prompts (higher i = more recent timestamp) get higher scores
189
+ score = 40.0 + i * 1.5
190
+ db.store_features(h, {"overall_score": score, "task_type": "test"})
191
+
192
+ response = handle_message({"type": "sync_prompts", "prompts": []}, db)
193
+ insights = response["insights"]
194
+ assert insights["score_trend"] == "improving"
195
+
196
+
197
+ def test_sync_insights_trend_declining(tmp_path: Path) -> None:
198
+ """Score trend should detect decline."""
199
+ db = PromptDB(tmp_path / "test.db")
200
+ import hashlib
201
+
202
+ for i in range(30):
203
+ ts = f"2026-03-{10 + i // 2:02d}T{10 + i % 24:02d}:00:00Z"
204
+ text = f"Debug the authentication middleware crash in production variant {i}"
205
+ db.insert_prompt(text, source="chatgpt-ext", project="test", session_id="s1", timestamp=ts)
206
+ h = hashlib.sha256(text.encode()).hexdigest()
207
+ # More recent = lower score
208
+ score = 85.0 - i * 1.5
209
+ db.store_features(h, {"overall_score": score, "task_type": "debug"})
210
+
211
+ response = handle_message({"type": "sync_prompts", "prompts": []}, db)
212
+ insights = response["insights"]
213
+ assert insights["score_trend"] == "declining"
214
+
215
+
216
+ # -- get_insights message type --
217
+
218
+
219
+ def test_handle_get_insights_empty(tmp_path: Path) -> None:
220
+ """get_insights on empty DB should return valid structure."""
221
+ db = PromptDB(tmp_path / "test.db")
222
+ response = handle_message({"type": "get_insights"}, db)
223
+ assert response["type"] == "insights_result"
224
+ assert response["avg_score"] == 0.0
225
+ assert response["prompt_count"] == 0
226
+ assert response["insights"] == []
227
+
228
+
229
+ def test_handle_get_insights_with_data(tmp_path: Path) -> None:
230
+ """get_insights should return analysis when data exists."""
231
+ db = PromptDB(tmp_path / "test.db")
232
+ import hashlib
233
+
234
+ for i in range(10):
235
+ text = f"Refactor the database connection pool to use async context managers v{i}"
236
+ db.insert_prompt(
237
+ text,
238
+ source="claude-ext",
239
+ project="test",
240
+ session_id="s1",
241
+ timestamp=f"2026-03-{15 + i}T10:00:00Z",
242
+ )
243
+ h = hashlib.sha256(text.encode()).hexdigest()
244
+ db.store_features(
245
+ h,
246
+ {
247
+ "overall_score": 55.0 + i,
248
+ "task_type": "refactor",
249
+ "key_instruction_position": 0.1,
250
+ "keyword_repetition_freq": 0.2,
251
+ "context_specificity": 0.4,
252
+ "has_constraints": False,
253
+ "ambiguity_score": 0.3,
254
+ "compressibility": 0.2,
255
+ "source": "claude-ext",
256
+ "token_count": 50,
257
+ },
258
+ )
259
+
260
+ response = handle_message({"type": "get_insights"}, db)
261
+ assert response["type"] == "insights_result"
262
+ assert response["prompt_count"] == 10
263
+ assert response["avg_score"] > 0
264
+
265
+
266
+ def test_handle_get_insights_with_source_filter(tmp_path: Path) -> None:
267
+ """get_insights should respect source filter."""
268
+ db = PromptDB(tmp_path / "test.db")
269
+ import hashlib
270
+
271
+ # ChatGPT prompts
272
+ for i in range(5):
273
+ text = f"Explain Python decorators with practical examples variant {i}"
274
+ db.insert_prompt(text, source="chatgpt-ext", project="t", session_id="s1")
275
+ h = hashlib.sha256(text.encode()).hexdigest()
276
+ db.store_features(
277
+ h,
278
+ {
279
+ "overall_score": 70.0,
280
+ "task_type": "explain",
281
+ "source": "chatgpt-ext",
282
+ "token_count": 30,
283
+ },
284
+ )
285
+
286
+ # Claude prompts
287
+ for i in range(5):
288
+ text = f"Create a REST API with FastAPI for user management variant {i}"
289
+ db.insert_prompt(text, source="claude-ext", project="t", session_id="s2")
290
+ h = hashlib.sha256(text.encode()).hexdigest()
291
+ db.store_features(
292
+ h,
293
+ {
294
+ "overall_score": 80.0,
295
+ "task_type": "implement",
296
+ "source": "claude-ext",
297
+ "token_count": 40,
298
+ },
299
+ )
300
+
301
+ # Filter to chatgpt only
302
+ response = handle_message({"type": "get_insights", "source": "chatgpt-ext"}, db)
303
+ assert response["type"] == "insights_result"
304
+ assert response["prompt_count"] == 5
305
+
306
+
307
+ # -- DB: get_recent_scores --
308
+
309
+
310
+ def test_get_recent_scores_empty(tmp_path: Path) -> None:
311
+ db = PromptDB(tmp_path / "test.db")
312
+ assert db.get_recent_scores() == []
313
+
314
+
315
+ def test_get_recent_scores_ordered(tmp_path: Path) -> None:
316
+ """Scores should be returned newest-first."""
317
+ db = PromptDB(tmp_path / "test.db")
318
+ import hashlib
319
+
320
+ texts = [
321
+ ("Oldest prompt about Python typing", "2026-03-01T10:00:00Z", 50.0),
322
+ ("Middle prompt about async patterns", "2026-03-15T10:00:00Z", 70.0),
323
+ ("Newest prompt about testing strategies", "2026-03-30T10:00:00Z", 90.0),
324
+ ]
325
+ for text, ts, score in texts:
326
+ db.insert_prompt(text, source="test", project="p", session_id="s", timestamp=ts)
327
+ h = hashlib.sha256(text.encode()).hexdigest()
328
+ db.store_features(h, {"overall_score": score, "task_type": "other"})
329
+
330
+ scores = db.get_recent_scores(limit=10)
331
+ assert len(scores) == 3
332
+ # Newest first
333
+ assert scores[0] == 90.0
334
+ assert scores[-1] == 50.0
335
+
336
+
337
+ def test_get_recent_scores_respects_limit(tmp_path: Path) -> None:
338
+ db = PromptDB(tmp_path / "test.db")
339
+ import hashlib
340
+
341
+ for i in range(20):
342
+ text = f"Generate unit tests for the payment processing module variant {i}"
343
+ db.insert_prompt(
344
+ text,
345
+ source="test",
346
+ project="p",
347
+ session_id="s",
348
+ timestamp=f"2026-03-{i + 1:02d}T10:00:00Z",
349
+ )
350
+ h = hashlib.sha256(text.encode()).hexdigest()
351
+ db.store_features(h, {"overall_score": 50.0 + i, "task_type": "test"})
352
+
353
+ scores = db.get_recent_scores(limit=5)
354
+ assert len(scores) == 5