reprompt-cli 1.2.0__tar.gz → 1.3.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 (229) hide show
  1. reprompt_cli-1.3.0/.testmondata +0 -0
  2. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.testmondata-shm +0 -0
  3. reprompt_cli-1.3.0/.testmondata-wal +0 -0
  4. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/CLAUDE.md +20 -5
  5. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/PKG-INFO +1 -1
  6. reprompt_cli-1.3.0/docs/superpowers/specs/2026-03-23-distill-design.md +375 -0
  7. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/pyproject.toml +1 -1
  8. reprompt_cli-1.3.0/src/reprompt/adapters/base.py +43 -0
  9. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/chatgpt.py +63 -0
  10. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/claude_code.py +103 -0
  11. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/cli.py +216 -0
  12. reprompt_cli-1.3.0/src/reprompt/core/conversation.py +60 -0
  13. reprompt_cli-1.3.0/src/reprompt/core/distill.py +317 -0
  14. reprompt_cli-1.3.0/src/reprompt/output/distill_terminal.py +94 -0
  15. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_compress.py +4 -1
  16. reprompt_cli-1.3.0/tests/test_conversation.py +120 -0
  17. reprompt_cli-1.3.0/tests/test_distill.py +368 -0
  18. reprompt_cli-1.3.0/tests/test_distill_cli.py +144 -0
  19. reprompt_cli-1.3.0/tests/test_parse_conversation_base.py +59 -0
  20. reprompt_cli-1.3.0/tests/test_parse_conversation_chatgpt.py +150 -0
  21. reprompt_cli-1.3.0/tests/test_parse_conversation_claude.py +160 -0
  22. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/uv.lock +1 -1
  23. reprompt_cli-1.2.0/.testmondata +0 -0
  24. reprompt_cli-1.2.0/.testmondata-wal +0 -0
  25. reprompt_cli-1.2.0/src/reprompt/adapters/base.py +0 -25
  26. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.editorconfig +0 -0
  27. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  28. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  29. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  30. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  31. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  32. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.github/dependabot.yml +0 -0
  33. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.github/workflows/ci.yml +0 -0
  34. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.github/workflows/publish.yml +0 -0
  35. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.gitignore +0 -0
  36. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/.pre-commit-config.yaml +0 -0
  37. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/CHANGELOG.md +0 -0
  38. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/CODE_OF_CONDUCT.md +0 -0
  39. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/CONTRIBUTING.md +0 -0
  40. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/LICENSE +0 -0
  41. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/README.md +0 -0
  42. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/SECURITY.md +0 -0
  43. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/action.yml +0 -0
  44. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/docs/launch-post.md +0 -0
  45. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/docs/roadmap.md +0 -0
  46. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/docs/superpowers/specs/2026-03-11-html-dashboard-design.md +0 -0
  47. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/docs/superpowers/specs/2026-03-11-merge-view-design.md +0 -0
  48. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/docs/superpowers/specs/2026-03-11-prompt-templates-design.md +0 -0
  49. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/docs/superpowers/specs/2026-03-22-prompt-compress-design.md +0 -0
  50. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/module.yaml +0 -0
  51. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/scripts/generate_demo_data.py +0 -0
  52. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/scripts/launch/hn_monitor.py +0 -0
  53. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/scripts/launch/reddit_helper.py +0 -0
  54. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/__init__.py +0 -0
  55. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/__init__.py +0 -0
  56. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/aider.py +0 -0
  57. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/claude_chat.py +0 -0
  58. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/cline.py +0 -0
  59. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/cursor.py +0 -0
  60. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/filters.py +0 -0
  61. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/gemini.py +0 -0
  62. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/adapters/openclaw.py +0 -0
  63. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/bridge/__init__.py +0 -0
  64. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/bridge/handler.py +0 -0
  65. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/bridge/host.py +0 -0
  66. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/bridge/manifest.py +0 -0
  67. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/bridge/protocol.py +0 -0
  68. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/commands/__init__.py +0 -0
  69. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/commands/telemetry.py +0 -0
  70. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/commands/wrapped.py +0 -0
  71. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/config.py +0 -0
  72. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/__init__.py +0 -0
  73. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/analyzer.py +0 -0
  74. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/compress.py +0 -0
  75. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/dedup.py +0 -0
  76. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/digest.py +0 -0
  77. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/effectiveness.py +0 -0
  78. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/extractors.py +0 -0
  79. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/extractors_zh.py +0 -0
  80. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/insights.py +0 -0
  81. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/lang_detect.py +0 -0
  82. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/library.py +0 -0
  83. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/lint.py +0 -0
  84. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/merge_view.py +0 -0
  85. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/models.py +0 -0
  86. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/persona.py +0 -0
  87. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/pipeline.py +0 -0
  88. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/privacy.py +0 -0
  89. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/prompt_dna.py +0 -0
  90. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/recommend.py +0 -0
  91. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/scorer.py +0 -0
  92. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/segmenter.py +0 -0
  93. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/session_meta.py +0 -0
  94. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/style.py +0 -0
  95. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/templates.py +0 -0
  96. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/timeutil.py +0 -0
  97. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/trends.py +0 -0
  98. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/core/wrapped.py +0 -0
  99. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/demo.py +0 -0
  100. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/embeddings/__init__.py +0 -0
  101. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/embeddings/base.py +0 -0
  102. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/embeddings/local_embed.py +0 -0
  103. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/embeddings/ollama.py +0 -0
  104. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/embeddings/openai_embed.py +0 -0
  105. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/embeddings/tfidf.py +0 -0
  106. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/mcp.py +0 -0
  107. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/mcp_main.py +0 -0
  108. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/output/__init__.py +0 -0
  109. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/output/chartjs.min.js +0 -0
  110. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/output/compress_terminal.py +0 -0
  111. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/output/html_report.py +0 -0
  112. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/output/json_out.py +0 -0
  113. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/output/markdown.py +0 -0
  114. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/output/terminal.py +0 -0
  115. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/output/wrapped_html.py +0 -0
  116. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/output/wrapped_terminal.py +0 -0
  117. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/py.typed +0 -0
  118. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/sharing/__init__.py +0 -0
  119. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/sharing/client.py +0 -0
  120. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/sharing/clipboard.py +0 -0
  121. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/storage/__init__.py +0 -0
  122. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/storage/db.py +0 -0
  123. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/telemetry/__init__.py +0 -0
  124. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/telemetry/collector.py +0 -0
  125. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/telemetry/consent.py +0 -0
  126. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/telemetry/events.py +0 -0
  127. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/telemetry/prompt.py +0 -0
  128. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/telemetry/queue.py +0 -0
  129. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/src/reprompt/telemetry/sender.py +0 -0
  130. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/__init__.py +0 -0
  131. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/conftest.py +0 -0
  132. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/fixtures/aider_chat_history.md +0 -0
  133. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/fixtures/chatgpt_conversations.json +0 -0
  134. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/fixtures/claude_chat_export.json +0 -0
  135. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/fixtures/claude_session.jsonl +0 -0
  136. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
  137. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/fixtures/gemini_session.json +0 -0
  138. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/fixtures/openclaw_session.jsonl +0 -0
  139. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_adapter_aider.py +0 -0
  140. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_adapter_chatgpt.py +0 -0
  141. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_adapter_claude.py +0 -0
  142. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_adapter_claude_chat.py +0 -0
  143. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_adapter_cline.py +0 -0
  144. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_adapter_gemini.py +0 -0
  145. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_adapter_openclaw.py +0 -0
  146. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_analyzer.py +0 -0
  147. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_bridge_cli.py +0 -0
  148. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_bridge_handler.py +0 -0
  149. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_bridge_integration.py +0 -0
  150. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_bridge_manifest.py +0 -0
  151. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_bridge_protocol.py +0 -0
  152. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_cli.py +0 -0
  153. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_cli_library_effectiveness.py +0 -0
  154. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_clipboard.py +0 -0
  155. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_compress_cli.py +0 -0
  156. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_compress_dna.py +0 -0
  157. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_compress_html.py +0 -0
  158. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_compress_insights.py +0 -0
  159. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_config.py +0 -0
  160. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_coverage_boost.py +0 -0
  161. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_cursor_adapter.py +0 -0
  162. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_db.py +0 -0
  163. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_db_digest.py +0 -0
  164. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_db_effectiveness.py +0 -0
  165. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_db_trends.py +0 -0
  166. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_dedup.py +0 -0
  167. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_demo.py +0 -0
  168. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_digest.py +0 -0
  169. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_digest_cli.py +0 -0
  170. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_e2e.py +0 -0
  171. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_effectiveness.py +0 -0
  172. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_embeddings_local.py +0 -0
  173. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_embeddings_ollama.py +0 -0
  174. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_embeddings_openai.py +0 -0
  175. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_empty_state.py +0 -0
  176. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_extractors.py +0 -0
  177. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_extractors_routing.py +0 -0
  178. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_extractors_zh.py +0 -0
  179. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_extractors_zh_e2e.py +0 -0
  180. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_html_report.py +0 -0
  181. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_import_cli.py +0 -0
  182. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_import_e2e.py +0 -0
  183. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_insights.py +0 -0
  184. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_insights_cli.py +0 -0
  185. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_install_hook.py +0 -0
  186. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_lang_detect.py +0 -0
  187. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_library.py +0 -0
  188. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_lint.py +0 -0
  189. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_lint_cli.py +0 -0
  190. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_markdown.py +0 -0
  191. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_mcp.py +0 -0
  192. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_merge_view.py +0 -0
  193. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_models.py +0 -0
  194. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_output.py +0 -0
  195. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_persona.py +0 -0
  196. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_pipeline.py +0 -0
  197. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_privacy.py +0 -0
  198. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_privacy_cli.py +0 -0
  199. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_privacy_e2e.py +0 -0
  200. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_privacy_output.py +0 -0
  201. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_prompt_dna.py +0 -0
  202. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_public_api.py +0 -0
  203. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_recommend.py +0 -0
  204. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_schema_version.py +0 -0
  205. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_score_cli.py +0 -0
  206. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_scorer.py +0 -0
  207. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_segmenter.py +0 -0
  208. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_share_e2e.py +0 -0
  209. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_sharing_client.py +0 -0
  210. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_style.py +0 -0
  211. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_telemetry_cli.py +0 -0
  212. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_telemetry_collector.py +0 -0
  213. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_telemetry_consent.py +0 -0
  214. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_telemetry_e2e.py +0 -0
  215. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_telemetry_events.py +0 -0
  216. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_telemetry_prompt.py +0 -0
  217. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_telemetry_queue.py +0 -0
  218. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_telemetry_sender.py +0 -0
  219. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_templates.py +0 -0
  220. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_timeutil.py +0 -0
  221. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_trends.py +0 -0
  222. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_trends_cli.py +0 -0
  223. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_use_cli.py +0 -0
  224. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_wrapped.py +0 -0
  225. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_wrapped_cli.py +0 -0
  226. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_wrapped_e2e.py +0 -0
  227. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_wrapped_html.py +0 -0
  228. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_wrapped_output.py +0 -0
  229. {reprompt_cli-1.2.0 → reprompt_cli-1.3.0}/tests/test_wrapped_share.py +0 -0
Binary file
Binary file
@@ -18,7 +18,7 @@ uv run python -m build # build wheel
18
18
 
19
19
  ```
20
20
  src/reprompt/
21
- ├── cli.py # Typer CLI (scan, import, report, search, library, recommend, demo, status, purge, install-hook, install-extension, extension-status, score, compare, insights, digest, style, use, privacy, compress) + plugin loading
21
+ ├── cli.py # Typer CLI (scan, import, report, search, library, recommend, demo, status, purge, install-hook, install-extension, extension-status, score, compare, insights, digest, style, use, privacy, compress, distill) + plugin loading
22
22
  ├── config.py # pydantic-settings, env vars (REPROMPT_ prefix) + TOML config
23
23
  ├── demo.py # Built-in demo data generator (no network required)
24
24
  ├── core/
@@ -40,9 +40,11 @@ src/reprompt/
40
40
  │ ├── persona.py # 6 prompt personas (Architect/Debugger/Explorer/Novelist/Sniper/Teacher)
41
41
  │ ├── wrapped.py # WrappedReport dataclass + build_wrapped(db) aggregation
42
42
  │ ├── privacy.py # Privacy metadata registry + exposure summary per adapter
43
- └── compress.py # 4-layer prompt compression (char norm + phrase simplify + filler delete + structure cleanup)
43
+ ├── compress.py # 4-layer prompt compression (char norm + phrase simplify + filler delete + structure cleanup)
44
+ │ ├── conversation.py # ConversationTurn, Conversation, DistillResult dataclasses
45
+ │ └── distill.py # 6-signal importance scoring + filtering + summary generation
44
46
  ├── adapters/
45
- │ ├── base.py # BaseAdapter ABC
47
+ │ ├── base.py # BaseAdapter ABC + parse_conversation() default
46
48
  │ ├── claude_code.py # Claude Code JSONL parser
47
49
  │ ├── openclaw.py # OpenClaw JSON parser (supports ~/.openclaw/ + legacy ~/.opencode/)
48
50
  │ ├── cursor.py # Cursor IDE .vscdb parser (cursorDiskKV + legacy ItemTable)
@@ -83,7 +85,8 @@ src/reprompt/
83
85
  ├── markdown.py # Markdown export
84
86
  ├── wrapped_terminal.py # Rich Prompt Wrapped report rendering
85
87
  ├── wrapped_html.py # Self-contained HTML share card (dark theme)
86
- └── compress_terminal.py # Rich output for compress command
88
+ ├── compress_terminal.py # Rich output for compress command
89
+ └── distill_terminal.py # Rich output for distill command
87
90
  ```
88
91
 
89
92
  ## Data Flow
@@ -120,7 +123,7 @@ reprompt-extension (private) ← Browser extension: Chrome/Firefox prompt capt
120
123
  - Pattern upsert (not clear+re-insert) for stable IDs
121
124
  - Prompts starting with `<` are filtered (system-injected XML)
122
125
  - Config: env vars (REPROMPT_ prefix) > TOML (~/.config/reprompt/config.toml) > defaults
123
- - Tests: pytest, 1153 tests, 95% coverage target
126
+ - Tests: pytest, 1217 tests, 95% coverage target
124
127
 
125
128
  ## Prompt Science Engine
126
129
 
@@ -144,3 +147,15 @@ Rule-based prompt optimization (added v1.2.0):
144
147
  - `compressibility` field in PromptDNA, visible in insights + HTML dashboard
145
148
 
146
149
  Sources: LLMLingua (Microsoft), CompactPrompt, TSC, stopwords-iso/zh, Prompt Report 2406.06608.
150
+
151
+ ## Conversation Distillation Engine
152
+
153
+ Conversation-level analysis (added v1.3.0):
154
+ - `reprompt distill` — extract important turns from AI conversations
155
+ - 6-signal importance scoring: position, length, tool_trigger, error_recovery, semantic_shift, uniqueness
156
+ - Hybrid data source: raw session files (full conversation) + DB enrichment
157
+ - `parse_conversation()` on adapters returns both user and assistant turns
158
+ - Claude Code and ChatGPT adapters have full implementations; others fall back to user-only
159
+ - `--last N` for recent sessions, `--summary` for compressed output, `--json`, `--copy`
160
+ - `--threshold` to control importance cutoff (default 0.3)
161
+ - Pro plugin interface: `reprompt.distill_backends` entry point for LLM summarization (future)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reprompt-cli
3
- Version: 1.2.0
3
+ Version: 1.3.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
@@ -0,0 +1,375 @@
1
+ # `reprompt distill` — Conversation Distillation Design
2
+
3
+ ## Goal
4
+
5
+ Extract the most important turns from an AI coding conversation, filtering noise and surfacing key decisions. Rule-based (Layer 1, open-source), with a plugin interface for future LLM-powered summarization (Layer 2, Pro).
6
+
7
+ ## Problem Statement
8
+
9
+ AI power users run 20–50+ turn conversations daily. When context windows fill up, they need to know: "What actually mattered in this session?" No existing tool provides conversation-level distillation locally, without sending data to a server.
10
+
11
+ **User stories:**
12
+ - "I had a 40-turn Claude Code session. What were the key decisions?"
13
+ - "Copy the important parts of my last conversation so I can paste them into a new session."
14
+ - "Show me which turns triggered the most work."
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ Raw session file → parse_conversation() → list[ConversationTurn]
20
+
21
+ DB enrichment (scores, dedup status)
22
+
23
+ Turn importance scoring (6 signals)
24
+
25
+ Filtering (threshold ≥ 0.3)
26
+
27
+ Turn pairing (user + assistant pairs)
28
+
29
+ Output (filtered / summary / json / clipboard)
30
+ ```
31
+
32
+ ### Data Source: Hybrid Approach
33
+
34
+ - **Raw session files** provide full conversation (user + assistant turns, tool_use, timestamps).
35
+ - **DB tables** provide enrichment (prompt scores from `prompt_features`, dedup status from `prompts`, session metadata from `session_meta`).
36
+ - Session files are located via the `processed_sessions` table which maps `file_path → source`.
37
+
38
+ This avoids storing conversation data twice while getting the full picture.
39
+
40
+ ## Data Model
41
+
42
+ ### ConversationTurn
43
+
44
+ ```python
45
+ @dataclass
46
+ class ConversationTurn:
47
+ role: str # "user" | "assistant"
48
+ text: str # The actual content
49
+ timestamp: str # ISO timestamp
50
+ turn_index: int # 0-based position in conversation
51
+
52
+ # Assistant-specific (0/False for user turns)
53
+ tool_calls: int = 0 # Number of tool_use blocks in this turn
54
+ has_error: bool = False # Whether turn contains error/failure
55
+ tool_use_paths: list[str] = field(default_factory=list) # File paths from tool_use blocks
56
+
57
+ # Enrichment (populated by distill engine, not adapter)
58
+ score: float | None = None # Display-only, from prompt_features (see Enrichment)
59
+ is_duplicate: bool = False # Cross-referenced with dedup
60
+ importance: float = 0.0 # Computed by distill scoring (6 signals only)
61
+ ```
62
+
63
+ **Note on `score` field:** This is **display-only enrichment data** — it does NOT feed into the 6-signal importance calculation. It is populated opportunistically from `prompt_features` (see Enrichment section) and shown in JSON output for reference. The `importance` field is the sole ranking signal.
64
+
65
+ ### Conversation
66
+
67
+ ```python
68
+ @dataclass
69
+ class Conversation:
70
+ session_id: str
71
+ source: str
72
+ project: str | None
73
+ turns: list[ConversationTurn]
74
+ start_time: str | None = None
75
+ end_time: str | None = None
76
+ duration_seconds: int | None = None # Computed from timestamps (see Duration)
77
+ ```
78
+
79
+ ### DistillResult
80
+
81
+ ```python
82
+ @dataclass
83
+ class DistillResult:
84
+ conversation: Conversation # Original full conversation
85
+ filtered_turns: list[ConversationTurn] # Turns above threshold
86
+ threshold: float # Importance threshold used
87
+ summary: str | None = None # Generated if --summary
88
+ files_changed: list[str] = field(default_factory=list) # Extracted from tool_use
89
+ stats: DistillStats = field(default_factory=DistillStats)
90
+
91
+ @dataclass
92
+ class DistillStats:
93
+ total_turns: int = 0
94
+ kept_turns: int = 0
95
+ retention_ratio: float = 0.0 # kept/total (0.26 = kept 26% of turns)
96
+ total_duration_seconds: int = 0
97
+ ```
98
+
99
+ ## Turn Importance Scoring
100
+
101
+ Six weighted signals, all rule-based (no LLM):
102
+
103
+ | Signal | Weight | Computation | Rationale |
104
+ |--------|--------|-------------|-----------|
105
+ | **Position** | 0.15 | First turn = 1.0, last turn = 0.8, others = 0.3 + 0.2 * recency | First turn sets context, last is conclusion |
106
+ | **Length** | 0.15 | `min(char_count / median_length, 1.0)` | Longer turns tend to be more substantive |
107
+ | **Tool trigger** | 0.20 | `min(tool_calls / 5, 1.0)` on the assistant turn following this user turn | Turns causing lots of tool use = key decisions |
108
+ | **Error recovery** | 0.15 | 1.0 if previous assistant turn has `has_error=True` | User correcting course = high-value context |
109
+ | **Semantic shift** | 0.20 | TF-IDF cosine distance from previous user turn; 0.5 (neutral) if first turn | Topic changes mark decision boundaries |
110
+ | **Uniqueness** | 0.15 | 1.0 - (similarity to most similar earlier turn via TF-IDF) | Novel instructions > repetitive ones |
111
+
112
+ **Scoring rules:**
113
+ - User turns: scored by all 6 signals → `importance = weighted sum`
114
+ - Assistant turns: derived score = average of adjacent user turns' importance
115
+ - Minimum threshold: 0.3 (configurable via `--threshold`)
116
+
117
+ ### Tool Trigger Pairing
118
+
119
+ The tool_trigger signal requires pairing user turns with their assistant responses. For user turn at index `i`, the tool_calls come from the assistant turn at index `i+1` (if it exists and role == "assistant").
120
+
121
+ ### Semantic Shift Computation
122
+
123
+ Uses scikit-learn `TfidfVectorizer` (already a dependency) fitted on all user turn texts in the conversation. Cosine distance between consecutive user turns. First user turn gets semantic_shift = 0.5 (neutral).
124
+
125
+ ## DB Enrichment
126
+
127
+ Enrichment is **opportunistic** — it enhances output when data exists, but distill works fully without it.
128
+
129
+ **Score enrichment:** For each user turn, compute `hashlib.sha256(text.strip().encode()).hexdigest()` (same algorithm as `Prompt.__post_init__`). Look up the hash in `prompt_features`. If found, populate `ConversationTurn.score` with `overall_score`. If not found (user never ran `reprompt score` on this text), leave as `None`. This field is display-only and does NOT affect the 6-signal importance calculation.
130
+
131
+ **Dedup enrichment:** Same hash lookup in `prompts` table. If `duplicate_of IS NOT NULL`, set `is_duplicate = True`. This is informational — the uniqueness signal in scoring uses TF-IDF similarity within the conversation, not the DB dedup status.
132
+
133
+ ### Duration Computation
134
+
135
+ `Conversation.duration_seconds` is derived from the first and last timestamps in `turns`:
136
+ ```python
137
+ from datetime import datetime
138
+ start = datetime.fromisoformat(turns[0].timestamp.replace("Z", "+00:00"))
139
+ end = datetime.fromisoformat(turns[-1].timestamp.replace("Z", "+00:00"))
140
+ duration_seconds = int((end - start).total_seconds())
141
+ ```
142
+ This follows the same `fromisoformat` + Z-replacement pattern used in `claude_code.py:parse_session_meta()`.
143
+
144
+ ## Turn Pairing in Output
145
+
146
+ Filtered output shows **user-assistant pairs**, not isolated turns. When a user turn passes the threshold, its corresponding assistant response is always included (preserves conversation coherence). Assistant-only turns below threshold are dropped only if their adjacent user turns are also below threshold.
147
+
148
+ ## Adapter Integration
149
+
150
+ ### Base class (additive, non-breaking)
151
+
152
+ ```python
153
+ # adapters/base.py
154
+ class BaseAdapter(ABC):
155
+ @abstractmethod
156
+ def parse_session(self, path: Path) -> list[Prompt]: ...
157
+
158
+ def parse_conversation(self, path: Path) -> list[ConversationTurn]:
159
+ """Parse full conversation with both roles.
160
+ Default: user-only turns from parse_session()."""
161
+ prompts = self.parse_session(path)
162
+ return [
163
+ ConversationTurn(
164
+ role="user", text=p.text, timestamp=p.timestamp, turn_index=i
165
+ )
166
+ for i, p in enumerate(prompts)
167
+ ]
168
+ ```
169
+
170
+ ### v1.3.0 adapters with full parse_conversation()
171
+
172
+ | Adapter | Approach |
173
+ |---------|----------|
174
+ | **claude-code** | Iterate JSONL entries. `type=user` + `role=user` → user turn. `type=assistant` → assistant turn. Count `tool_use` content blocks. Detect errors from `is_error` field or error-related content. |
175
+ | **chatgpt-export** | Walk `mapping` tree in order for a single conversation (selected by `conv_id`). `author.role=user` → user turn. `author.role=assistant` → assistant turn. No tool_calls data (set to 0). Accepts optional `conv_id` parameter; if None, parses first conversation in file. |
176
+
177
+ ### Future adapters (v1.3.1+)
178
+
179
+ claude-chat, aider, gemini, openclaw, cline — implement `parse_conversation()` as data becomes available. They fall back to user-only via base class until then.
180
+
181
+ ## Session Resolution
182
+
183
+ How `distill` finds which session file to parse:
184
+
185
+ | Input | Resolution |
186
+ |-------|-----------|
187
+ | `--last N` | Query `processed_sessions` ordered by `processed_at DESC`, take first N. Return list of `(file_path, source)`. |
188
+ | `<session_id>` | Query `prompts` for matching `session_id`, get `source`. Reconstruct file path: `adapter.default_session_path / session_id + ext` (e.g. `.jsonl` for claude-code). Verify file exists. If not, fall back to `processed_sessions WHERE file_path LIKE '%' || session_id || '%'`. |
189
+ | `--source X` | Filter `processed_sessions` by source, then apply `--last` logic. |
190
+
191
+ **Edge case — file deleted:** If session file no longer exists on disk, distill falls back to DB-only mode: query `prompts WHERE session_id = ?` for user turns (no assistant turns, no tool_use data). Warn user: "Session file not found, showing user turns only."
192
+
193
+ ### ChatGPT Session Resolution
194
+
195
+ ChatGPT exports contain multiple conversations in one `conversations.json` file. The `session_id` for ChatGPT prompts is a hash-based string (e.g. `chatgpt-20260323T100000-ab1c2d3e`). When `distill` receives a ChatGPT session_id:
196
+
197
+ 1. Look up `source = "chatgpt-export"` from `prompts` table
198
+ 2. Find the `conversations.json` file path from `processed_sessions`
199
+ 3. Parse the file, iterate conversation objects, match by `conv_id` (computed same way as in `chatgpt.py:_make_session_id()`)
200
+ 4. Call `parse_conversation()` on that single conversation object
201
+
202
+ The ChatGPT `parse_conversation()` accepts an optional `conv_id` parameter to select a specific conversation from the file.
203
+
204
+ ## Output Modes
205
+
206
+ ### Tier 1 — Filtered conversation (default)
207
+
208
+ ```
209
+ ╭─ Distill: session abc123 (claude-code) ─╮
210
+ │ Project: reprompt | 45min | 47 → 12 turns │
211
+ ╰──────────────────────────────────────────╯
212
+
213
+ ★★★ [T1] User:
214
+ 根据我们的设计文档继续我们的任务
215
+ Assistant: I'll read the spec and continue with Task 5...
216
+
217
+ ★★☆ [T8] User:
218
+ 不对,用approach B,parse_session不要改
219
+ Assistant: You're right, creating separate parse_conversation()...
220
+
221
+ ★★★ [T15] User:
222
+ 测试通过了,commit吧
223
+ Assistant: Committed: feat: add conversation parser
224
+ ```
225
+
226
+ Stars: ★★★ = importance ≥ 0.7, ★★☆ = ≥ 0.5, ★☆☆ = ≥ 0.3
227
+
228
+ Assistant text truncated to first 80 chars in terminal mode (full in JSON).
229
+
230
+ ### Tier 2 — Summary (`--summary`)
231
+
232
+ ```
233
+ ╭─ Session Summary: abc123 ─╮
234
+
235
+ This session implemented the conversation parser for the distill feature.
236
+
237
+ Key decisions:
238
+ • Created separate parse_conversation() instead of modifying parse_session()
239
+ • Used weighted scoring with 6 signals for turn importance
240
+ • Threshold set at 0.3 for filtering
241
+
242
+ Files changed: core/distill.py, adapters/base.py, tests/test_distill.py
243
+
244
+ 47 turns → 12 key turns | 45min | claude-code
245
+ ╰───────────────────────────╯
246
+ ```
247
+
248
+ Summary is rule-based:
249
+ 1. **Description**: First user turn text (compressed via `compress_text()`) + project name
250
+ 2. **Key decisions**: Top 5 user turns by importance, each compressed
251
+ 3. **Files changed**: Flattened from `tool_use_paths` across all assistant turns (deduplicated, sorted). Only Edit/Write paths included (Read paths are noise).
252
+ 4. **Stats**: Turn counts + duration + source
253
+
254
+ ### Tier 3 — JSON (`--json`)
255
+
256
+ ```json
257
+ {
258
+ "session_id": "abc123",
259
+ "source": "claude-code",
260
+ "project": "reprompt",
261
+ "duration_seconds": 2700,
262
+ "total_turns": 47,
263
+ "kept_turns": 12,
264
+ "retention_ratio": 0.26,
265
+ "threshold": 0.3,
266
+ "summary": "...",
267
+ "files_changed": ["core/distill.py", "adapters/base.py"],
268
+ "turns": [
269
+ {
270
+ "turn_index": 0,
271
+ "role": "user",
272
+ "text": "...",
273
+ "timestamp": "2026-03-23T10:00:00Z",
274
+ "importance": 0.85,
275
+ "tool_calls": 0
276
+ }
277
+ ]
278
+ }
279
+ ```
280
+
281
+ ## CLI Interface
282
+
283
+ ```
284
+ reprompt distill # Default: most recent session
285
+ reprompt distill --last 3 # Last 3 sessions
286
+ reprompt distill --summary # Key decisions only
287
+ reprompt distill --copy # Filtered → clipboard
288
+ reprompt distill --json # Machine-readable
289
+ reprompt distill abc123 # By session ID
290
+ reprompt distill --source chatgpt-export # Filter by adapter
291
+ reprompt distill --threshold 0.5 # Stricter filtering
292
+ reprompt distill --last 3 --copy # Last 3, concatenated to clipboard
293
+ ```
294
+
295
+ ### Flag Details
296
+
297
+ | Flag | Type | Default | Description |
298
+ |------|------|---------|-------------|
299
+ | `--last` | `int` | 1 | Most recent N sessions. Always requires explicit value. |
300
+ | `--source` | `str` | None | Filter by adapter name |
301
+ | `--summary` | `bool` | False | Tier 2 compressed output |
302
+ | `--json` | `bool` | False | JSON output |
303
+ | `--copy` | `bool` | False | Copy to clipboard |
304
+ | `--threshold` | `float` | 0.3 | Importance cutoff (0.0–1.0) |
305
+ | `session_id` | `str` (positional, optional) | None | Specific session |
306
+
307
+ `session_id` and `--last` are mutually exclusive. If neither is provided, default behavior is `--last 1` (most recent session).
308
+
309
+ **Multi-session behavior (`--last N`):** Each session is distilled independently. Terminal output separates sessions with a horizontal rule. `--copy` concatenates all sessions (separated by `---`). `--json` outputs a JSON array of DistillResult objects.
310
+
311
+ ## File Structure
312
+
313
+ | File | Responsibility |
314
+ |------|----------------|
315
+ | `core/conversation.py` | `ConversationTurn`, `Conversation`, `DistillResult`, `DistillStats` dataclasses |
316
+ | `core/distill.py` | `distill_conversation(conv, threshold) → DistillResult` — scoring engine, filtering, summary |
317
+ | `adapters/base.py` | Add `parse_conversation()` default method |
318
+ | `adapters/claude_code.py` | Override `parse_conversation()` with full JSONL parsing |
319
+ | `adapters/chatgpt.py` | Override `parse_conversation()` with tree-walk parsing |
320
+ | `output/distill_terminal.py` | `render_distill(result) → str` Rich terminal output |
321
+ | `cli.py` | `distill` command registration |
322
+
323
+ ## DB Changes
324
+
325
+ **None.** Distill reads from existing tables:
326
+ - `processed_sessions` — file path + source lookup
327
+ - `prompt_features` — optional score enrichment
328
+ - `prompts` — dedup status, fallback if file missing
329
+
330
+ No new tables, no migrations.
331
+
332
+ ## Pro Plugin Interface (Layer 2, future)
333
+
334
+ ```python
335
+ # Entry point group: "reprompt.distill_backends"
336
+ # Interface:
337
+ def distill_llm(conversation: Conversation) -> str:
338
+ """Returns LLM-generated semantic summary.
339
+ Example: '20 turns about auth → decided JWT with refresh tokens'
340
+ """
341
+ ```
342
+
343
+ When `reprompt-pro` is installed and user passes `--mode llm`:
344
+ - Plugin registered via `entry_points`
345
+ - Falls back gracefully: "LLM distillation requires reprompt-pro. Install: pip install reprompt-pro"
346
+
347
+ ## Testing Strategy
348
+
349
+ | Test file | Coverage |
350
+ |-----------|----------|
351
+ | `tests/test_conversation.py` | Dataclass construction, field defaults, validation |
352
+ | `tests/test_distill.py` | Scoring signals (each independently), filtering, summary generation, edge cases (empty conversation, single turn, all below threshold) |
353
+ | `tests/test_distill_cli.py` | CLI invocation, --last, --json, --summary, --copy, --threshold, session_id |
354
+ | `tests/test_parse_conversation_claude.py` | Claude Code parse_conversation() with sample JSONL |
355
+ | `tests/test_parse_conversation_chatgpt.py` | ChatGPT parse_conversation() with sample JSON, conv_id selection |
356
+
357
+ **Additional edge case tests (in test_distill.py and test_distill_cli.py):**
358
+ - Session file deleted → DB-only fallback (user turns only, warning message)
359
+ - `--last 3` multi-session → each session distilled independently, separated output
360
+ - `--copy` with `--last 3` → concatenated with `---` separator
361
+ - Empty conversation (0 turns) → graceful empty result
362
+ - Single-turn conversation → that turn always passes threshold
363
+ - All turns below threshold → empty filtered_turns with stats showing 0 kept
364
+
365
+ **Test data:** Synthetic JSONL/JSON fixtures embedded in test files (same pattern as existing adapter tests).
366
+
367
+ ## Success Criteria
368
+
369
+ 1. `reprompt distill --last` produces a useful filtered view of the most recent session
370
+ 2. 47-turn conversation distilled to ~10-15 key turns (70-80% reduction)
371
+ 3. Error-recovery turns and topic-shift turns are consistently ranked high
372
+ 4. `--copy` puts clipboard-ready text that can be pasted into a new AI session
373
+ 5. Works with both Claude Code and ChatGPT exports
374
+ 6. Zero new dependencies (uses existing scikit-learn for TF-IDF)
375
+ 7. All tests pass, no regressions in existing 1153 tests
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reprompt-cli"
3
- version = "1.2.0"
3
+ version = "1.3.0"
4
4
  description = "Discover, analyze, and optimize your prompts from AI coding sessions"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -0,0 +1,43 @@
1
+ """Base adapter interface for AI coding session parsers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from pathlib import Path
7
+
8
+ from reprompt.core.conversation import ConversationTurn
9
+ from reprompt.core.models import Prompt
10
+
11
+
12
+ class BaseAdapter(ABC):
13
+ """Abstract base class for session adapters."""
14
+
15
+ name: str
16
+ default_session_path: str
17
+
18
+ @abstractmethod
19
+ def parse_session(self, path: Path) -> list[Prompt]:
20
+ """Parse a session file and return a list of Prompt objects."""
21
+ ...
22
+
23
+ @abstractmethod
24
+ def detect_installed(self) -> bool:
25
+ """Check if the tool's session directory exists."""
26
+ ...
27
+
28
+ def parse_conversation(self, path: Path) -> list[ConversationTurn]:
29
+ """Parse full conversation with both roles.
30
+
31
+ Default implementation wraps parse_session() results as user-only turns.
32
+ Override in adapters that can extract assistant turns.
33
+ """
34
+ prompts = self.parse_session(path)
35
+ return [
36
+ ConversationTurn(
37
+ role="user",
38
+ text=p.text,
39
+ timestamp=p.timestamp,
40
+ turn_index=i,
41
+ )
42
+ for i, p in enumerate(prompts)
43
+ ]
@@ -16,6 +16,7 @@ from pathlib import Path
16
16
 
17
17
  from reprompt.adapters.base import BaseAdapter
18
18
  from reprompt.adapters.filters import should_keep_prompt
19
+ from reprompt.core.conversation import ConversationTurn
19
20
  from reprompt.core.models import Prompt
20
21
 
21
22
 
@@ -74,6 +75,68 @@ class ChatGPTAdapter(BaseAdapter):
74
75
 
75
76
  return prompts
76
77
 
78
+ def parse_conversation(
79
+ self, path: Path, conv_id: str | None = None
80
+ ) -> list[ConversationTurn]:
81
+ """Parse a single conversation with both user and assistant turns.
82
+
83
+ Args:
84
+ path: Path to conversations.json file.
85
+ conv_id: If given, select the conversation matching this ID.
86
+ If None, parse the first conversation in the file.
87
+ """
88
+ try:
89
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
90
+ except (OSError, json.JSONDecodeError, UnicodeDecodeError):
91
+ return []
92
+
93
+ if not isinstance(data, list) or not data:
94
+ return []
95
+
96
+ # Select conversation
97
+ conversation = None
98
+ if conv_id:
99
+ for conv in data:
100
+ if _make_session_id(conv) == conv_id:
101
+ conversation = conv
102
+ break
103
+ if conversation is None:
104
+ return []
105
+ else:
106
+ conversation = data[0]
107
+
108
+ mapping = conversation.get("mapping", {})
109
+
110
+ # Collect all messages (user + assistant) sorted by create_time
111
+ all_nodes = []
112
+ for node in mapping.values():
113
+ msg = node.get("message")
114
+ if msg is None:
115
+ continue
116
+ author = msg.get("author", {})
117
+ role = author.get("role", "")
118
+ if role not in ("user", "assistant"):
119
+ continue
120
+ all_nodes.append((role, msg))
121
+
122
+ all_nodes.sort(key=lambda x: x[1].get("create_time") or 0)
123
+
124
+ turns: list[ConversationTurn] = []
125
+ for i, (role, msg) in enumerate(all_nodes):
126
+ text = _extract_content(msg)
127
+ if not text.strip():
128
+ continue
129
+ turns.append(
130
+ ConversationTurn(
131
+ role=role,
132
+ text=text,
133
+ timestamp=_format_timestamp(msg.get("create_time")),
134
+ turn_index=i,
135
+ )
136
+ )
137
+
138
+ return turns
139
+
77
140
 
78
141
  def _make_session_id(conversation: dict) -> str:
79
142
  """Create a stable session ID from conversation create_time + title hash."""