claude-smart 0.2.41 → 0.2.43

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 (396) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +86 -48
  4. package/package.json +10 -3
  5. package/plugin/.claude-plugin/plugin.json +9 -3
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +2 -2
  8. package/plugin/dashboard/next.config.ts +9 -1
  9. package/plugin/pyproject.toml +2 -2
  10. package/plugin/scripts/_lib.sh +91 -0
  11. package/plugin/scripts/backend-service.sh +46 -15
  12. package/plugin/scripts/cli.sh +29 -1
  13. package/plugin/scripts/codex-hook.js +72 -4
  14. package/plugin/scripts/dashboard-build.sh +1 -0
  15. package/plugin/scripts/dashboard-service.sh +1 -0
  16. package/plugin/scripts/ensure-plugin-root.sh +7 -14
  17. package/plugin/scripts/hook_entry.sh +1 -0
  18. package/plugin/scripts/smart-install.sh +18 -2
  19. package/plugin/src/claude_smart/cli.py +72 -38
  20. package/plugin/src/claude_smart/context_format.py +11 -12
  21. package/plugin/src/claude_smart/cs_cite.py +26 -12
  22. package/plugin/src/claude_smart/ids.py +13 -5
  23. package/plugin/uv.lock +1 -1
  24. package/plugin/vendor/reflexio/.env.example +53 -0
  25. package/plugin/vendor/reflexio/LICENSE +201 -0
  26. package/plugin/vendor/reflexio/README.md +338 -0
  27. package/plugin/vendor/reflexio/pyproject.toml +271 -0
  28. package/plugin/vendor/reflexio/reflexio/README.md +184 -0
  29. package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
  30. package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
  31. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
  32. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
  33. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
  34. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
  35. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
  36. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
  37. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
  38. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
  39. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
  40. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
  41. package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
  42. package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
  43. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
  44. package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
  45. package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
  46. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
  47. package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
  48. package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
  49. package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
  50. package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
  51. package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
  52. package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
  53. package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
  54. package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
  63. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
  64. package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
  65. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
  66. package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
  67. package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
  69. package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
  70. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
  71. package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
  72. package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
  73. package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
  74. package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
  75. package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
  145. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
  146. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
  147. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
  148. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
  149. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
  150. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
  151. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
  152. package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
  153. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
  154. package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
  155. package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
  156. package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
  157. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
  158. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
  159. package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
  160. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
  161. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
  162. package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
  163. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
  164. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
  165. package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
  166. package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
  167. package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
  168. package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
  169. package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
  170. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
  171. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
  172. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
  173. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
  174. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
  175. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
  176. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
  177. package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
  178. package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
  179. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
  180. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
  181. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
  182. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
  183. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
  184. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
  185. package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
  186. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
  187. package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
  188. package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
  189. package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
  190. package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
  191. package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
  192. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
  193. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
  194. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
  195. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
  196. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
  197. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
  198. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
  199. package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
  200. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
  201. package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
  202. package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
  203. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
  204. package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
  205. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
  206. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
  207. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
  208. package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
  209. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
  210. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
  211. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
  212. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
  213. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
  214. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
  215. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
  216. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
  217. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
  218. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
  219. package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
  220. package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
  221. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
  222. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
  223. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
  224. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
  225. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
  226. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
  227. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
  228. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
  229. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
  230. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
  231. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
  232. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
  233. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
  234. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
  235. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
  236. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
  237. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
  238. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
  239. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
  240. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
  241. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
  242. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
  243. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
  244. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
  245. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
  246. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
  247. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
  248. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
  249. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
  250. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
  251. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
  252. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
  253. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
  254. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
  255. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
  256. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
  257. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
  258. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
  259. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
  260. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
  261. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
  262. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
  263. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
  264. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
  265. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
  266. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
  267. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
  268. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
  269. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
  270. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
  271. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
  272. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
  273. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
  274. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
  275. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
  276. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
  277. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
  278. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
  279. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
  282. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
  289. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
  290. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
  292. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
  296. package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
  297. package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
  311. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
  312. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
  313. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
  318. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
  319. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
  320. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
  321. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
  322. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
  323. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
  324. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
  325. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
  326. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
  327. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
  328. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
  329. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
  330. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
  331. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
  332. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
  333. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
  334. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
  335. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
  336. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
  337. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
  338. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
  339. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
  340. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
  341. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
  342. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
  343. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
  344. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
  345. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
  346. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
  347. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
  348. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
  349. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
  350. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
  351. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
  352. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
  353. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
  354. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
  355. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
  356. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
  357. package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
  358. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
  359. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
  360. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
  361. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
  362. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
  363. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
  364. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
  365. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
  366. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
  367. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
  368. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
  369. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
  370. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
  371. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
  372. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
  373. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
  374. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
  375. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
  376. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
  377. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
  378. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
  379. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
  380. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
  381. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
  382. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
  383. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
  384. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
  385. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
  386. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
  387. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
  388. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
  389. package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
  390. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
  391. package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
  392. package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
  393. package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
  394. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
  395. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
  396. package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
@@ -0,0 +1,1122 @@
1
+ """Claude Code CLI as a LiteLLM custom provider.
2
+
3
+ Routes ``litellm.completion(model="claude-code/...", ...)`` through the
4
+ user's locally-installed ``claude`` CLI (the Claude Code binary), so
5
+ reflexio's extractors can run with no external LLM API key — they reuse
6
+ whatever auth the user already has for Claude Code.
7
+
8
+ Activation is opt-in via ``CLAUDE_SMART_USE_LOCAL_CLI=1``. Without it,
9
+ the provider does not register and reflexio falls back to its normal
10
+ OpenAI/Anthropic/etc. provider priority.
11
+
12
+ Structured output: when callers pass a Pydantic ``response_format``,
13
+ the JSON schema is appended to the system prompt instructing the CLI
14
+ to reply with matching JSON. The CLI's text reply is returned as
15
+ ``message.content``; ``LiteLLMClient._maybe_parse_structured_output``
16
+ then parses it into the Pydantic instance via the existing pipeline.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import inspect
22
+ import json
23
+ import logging
24
+ import os
25
+ import re
26
+ import shutil
27
+ import subprocess # noqa: S404 — subprocess is the integration point; inputs are sanitised.
28
+ import tempfile
29
+ import time
30
+ from contextlib import suppress
31
+ from datetime import UTC, datetime
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ import litellm
36
+ from litellm.llms.custom_llm import CustomLLM
37
+ from litellm.types.utils import (
38
+ ChatCompletionMessageToolCall,
39
+ Choices,
40
+ Function,
41
+ Message,
42
+ ModelResponse,
43
+ Usage,
44
+ )
45
+ from pydantic import BaseModel
46
+
47
+ from reflexio.server.llm.providers.claude_code_stream_parser import (
48
+ ParseResult,
49
+ classify_stall,
50
+ parse_reset_estimate,
51
+ parse_stream_json,
52
+ )
53
+
54
+ _LOGGER = logging.getLogger(__name__)
55
+
56
+ PROVIDER_KEY = "claude-code"
57
+ ENV_ENABLE = "CLAUDE_SMART_USE_LOCAL_CLI"
58
+ _ENV_CLI_PATH = "CLAUDE_SMART_CLI_PATH"
59
+ _ENV_HOST = "CLAUDE_SMART_HOST"
60
+ _ENV_CODEX_PATH = "CLAUDE_SMART_CODEX_PATH"
61
+ _ENV_TIMEOUT = "CLAUDE_SMART_CLI_TIMEOUT"
62
+ _ENV_MODEL = "CLAUDE_SMART_CLI_MODEL"
63
+ _HOST_CODEX = "codex"
64
+ _HOST_CLAUDE_CODE = "claude-code"
65
+ _CODEX_COMPAT_SCRIPT_NAMES = (
66
+ ("codex-claude-compat.cmd", "codex-claude-compat")
67
+ if os.name == "nt"
68
+ else ("codex-claude-compat", "codex-claude-compat.cmd")
69
+ )
70
+ _CODEX_COMPAT_SCRIPT_NAME_SET = set(_CODEX_COMPAT_SCRIPT_NAMES)
71
+ _DEFAULT_TIMEOUT_SECONDS = 120
72
+ _DEFAULT_CLI_MODEL = "claude-sonnet-4-6"
73
+
74
+ _TRUTHY_ENV_VALUES = {"1", "true", "yes"}
75
+ _UNSUPPORTED_PARAMS_WARNED: set[str] = set()
76
+ _IMAGE_WARNED = False
77
+ _MULTITURN_WARNED = False
78
+
79
+
80
+ class ClaudeCodeCLIError(RuntimeError):
81
+ """Raised when the claude CLI subprocess fails in a way we cannot recover from."""
82
+
83
+
84
+ def _env_enabled() -> bool:
85
+ """Return True when ``CLAUDE_SMART_USE_LOCAL_CLI`` is set to a truthy value.
86
+
87
+ Returns:
88
+ bool: True if the opt-in env var is set, False otherwise.
89
+ """
90
+ raw = os.environ.get(ENV_ENABLE)
91
+ return bool(raw) and raw.lower() in _TRUTHY_ENV_VALUES
92
+
93
+
94
+ def _host() -> str:
95
+ """Return the host that owns this backend process."""
96
+ return (
97
+ _HOST_CODEX if os.environ.get(_ENV_HOST) == _HOST_CODEX else _HOST_CLAUDE_CODE
98
+ )
99
+
100
+
101
+ def _cli_name() -> str:
102
+ """Return the expected local CLI binary for the active host."""
103
+ return "codex" if _host() == _HOST_CODEX else "claude"
104
+
105
+
106
+ def _candidate_codex_compat_path() -> Path | None:
107
+ """Return the Codex compatibility wrapper from plugin roots, if present."""
108
+ for env_var in ("PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"):
109
+ root = os.environ.get(env_var)
110
+ if not root:
111
+ continue
112
+ for name in _CODEX_COMPAT_SCRIPT_NAMES:
113
+ candidate = Path(root) / "scripts" / name
114
+ if candidate.is_file() and os.access(candidate, os.X_OK):
115
+ return candidate
116
+ return None
117
+
118
+
119
+ def _resolve_cli_path() -> str | None:
120
+ """Return the path to the active host CLI, or None if unavailable.
121
+
122
+ Honours the ``CLAUDE_SMART_CLI_PATH`` override before falling back to
123
+ host-specific defaults. Claude Code uses ``claude``. Codex prefers the
124
+ compatibility wrapper shipped with the plugin, then falls back to
125
+ ``codex`` directly.
126
+
127
+ Returns:
128
+ str | None: Absolute path to the CLI, or None if not found.
129
+ """
130
+ override = os.environ.get(_ENV_CLI_PATH)
131
+ if override:
132
+ candidate = Path(override)
133
+ if candidate.is_file() and os.access(candidate, os.X_OK):
134
+ return str(candidate)
135
+ _LOGGER.warning(
136
+ "%s=%s is not an executable file; falling back to PATH",
137
+ _ENV_CLI_PATH,
138
+ override,
139
+ )
140
+ if _host() == _HOST_CODEX:
141
+ compat = _candidate_codex_compat_path()
142
+ if compat is not None:
143
+ return str(compat)
144
+ return os.environ.get(_ENV_CODEX_PATH) or shutil.which("codex")
145
+ return shutil.which("claude")
146
+
147
+
148
+ def is_claude_code_available() -> bool:
149
+ """Return True when the local CLI provider is usable right now.
150
+
151
+ Both the opt-in env var *and* a resolvable CLI path are required, so
152
+ an unrelated env var can't silently redirect extraction traffic.
153
+
154
+ Returns:
155
+ bool: True iff ``CLAUDE_SMART_USE_LOCAL_CLI`` is truthy AND a
156
+ host CLI is resolvable.
157
+ """
158
+ return _env_enabled() and _resolve_cli_path() is not None
159
+
160
+
161
+ def _flatten_content(content: Any) -> str:
162
+ """Collapse LiteLLM content (string or content-block list) to plain text.
163
+
164
+ Image blocks are silently skipped with a one-time WARN log (see
165
+ ``_warn_image_dropped_once``). cache_control markers are ignored
166
+ since the CLI does not accept them.
167
+
168
+ Args:
169
+ content: LiteLLM content — string, list of content blocks, or None.
170
+
171
+ Returns:
172
+ str: Plain-text content; empty string if no text survives.
173
+ """
174
+ if content is None:
175
+ return ""
176
+ if isinstance(content, str):
177
+ return content
178
+ if isinstance(content, list):
179
+ parts: list[str] = []
180
+ for block in content:
181
+ if isinstance(block, dict):
182
+ block_type = block.get("type")
183
+ if block_type in {"image", "image_url"}:
184
+ _warn_image_dropped_once()
185
+ continue
186
+ text = block.get("text") or block.get("content")
187
+ if isinstance(text, str):
188
+ parts.append(text)
189
+ elif isinstance(block, str):
190
+ parts.append(block)
191
+ return "\n".join(parts)
192
+ return str(content)
193
+
194
+
195
+ def _warn_image_dropped_once() -> None:
196
+ """Emit a single WARN when image content is dropped by the CLI bridge.
197
+
198
+ Returns:
199
+ None
200
+ """
201
+ global _IMAGE_WARNED
202
+ if not _IMAGE_WARNED:
203
+ _LOGGER.warning(
204
+ "claude-code provider: image content blocks are dropped — "
205
+ "the CLI bridge accepts text only. Install an image-capable "
206
+ "LLM provider for screenshot analysis."
207
+ )
208
+ _IMAGE_WARNED = True
209
+
210
+
211
+ def _warn_multiturn_once() -> None:
212
+ """Emit a single WARN when multi-turn context gets flattened to text.
213
+
214
+ Returns:
215
+ None
216
+ """
217
+ global _MULTITURN_WARNED
218
+ if not _MULTITURN_WARNED:
219
+ _LOGGER.warning(
220
+ "claude-code provider: multi-turn messages are flattened into a "
221
+ "single 'User:/Assistant:' transcript. Quality may differ from "
222
+ "the Anthropic messages API."
223
+ )
224
+ _MULTITURN_WARNED = True
225
+
226
+
227
+ def _warn_unsupported_param_once(name: str) -> None:
228
+ """Emit a single WARN when a LiteLLM param has no CLI equivalent.
229
+
230
+ Args:
231
+ name: Parameter name that was ignored (e.g. ``"temperature"``).
232
+
233
+ Returns:
234
+ None
235
+ """
236
+ if name in _UNSUPPORTED_PARAMS_WARNED:
237
+ return
238
+ _UNSUPPORTED_PARAMS_WARNED.add(name)
239
+ _LOGGER.warning(
240
+ "claude-code provider: ignoring unsupported parameter %r — "
241
+ "the CLI does not expose this control.",
242
+ name,
243
+ )
244
+
245
+
246
+ def _schema_instruction(response_format: Any) -> str | None:
247
+ """Build a schema instruction to append to the system prompt.
248
+
249
+ Accepts either a Pydantic model class, a LiteLLM ``json_schema``
250
+ response_format dict, or a plain JSON-schema dict. Returns None
251
+ when nothing usable is found — callers fall through to unstructured
252
+ completion.
253
+
254
+ Args:
255
+ response_format: The response_format value from LiteLLM kwargs
256
+ or ``optional_params`` — a Pydantic class or a dict.
257
+
258
+ Returns:
259
+ str | None: Instruction text to append to the system prompt,
260
+ or None if no schema could be extracted.
261
+ """
262
+ schema = _extract_json_schema(response_format)
263
+ if not schema:
264
+ return None
265
+ return (
266
+ "You MUST respond with a single JSON object that strictly matches "
267
+ "the schema below. Output JSON only — no markdown fences, no prose, "
268
+ "no explanation.\n\n"
269
+ f"Schema:\n{json.dumps(schema, indent=2)}"
270
+ )
271
+
272
+
273
+ def _extract_json_schema(response_format: Any) -> dict[str, Any] | None:
274
+ """Extract a JSON schema from LiteLLM's response_format values.
275
+
276
+ Args:
277
+ response_format: Pydantic class, LiteLLM dict
278
+ (``{"type": "json_schema", "json_schema": {"schema": ...}}``),
279
+ or a raw JSON-schema dict.
280
+
281
+ Returns:
282
+ dict | None: The JSON schema, or None if one cannot be extracted.
283
+ """
284
+ if response_format is None:
285
+ return None
286
+ if inspect.isclass(response_format) and issubclass(response_format, BaseModel):
287
+ return response_format.model_json_schema()
288
+ if isinstance(response_format, dict):
289
+ if response_format.get("type") == "json_schema":
290
+ inner = response_format.get("json_schema") or {}
291
+ if isinstance(inner, dict):
292
+ schema = inner.get("schema") or inner
293
+ if isinstance(schema, dict):
294
+ return schema
295
+ if "properties" in response_format or "$ref" in response_format:
296
+ return response_format
297
+ return None
298
+
299
+
300
+ def _split_system_and_dialogue(
301
+ messages: list[dict[str, Any]],
302
+ ) -> tuple[str, str]:
303
+ """Split chat messages into (system_prompt, dialogue) for the CLI.
304
+
305
+ The ``claude -p`` CLI takes one stdin prompt and an optional
306
+ ``--append-system-prompt``. Multi-turn context is flattened into a
307
+ single textual dialogue prefixed with role labels, since the CLI
308
+ does not accept a messages array.
309
+
310
+ System messages are merged (joined with blank lines) and returned
311
+ separately for the ``--append-system-prompt`` flag. ``tool`` role
312
+ messages are folded in as ``Tool:`` lines.
313
+
314
+ Args:
315
+ messages: LiteLLM-style chat messages.
316
+
317
+ Returns:
318
+ tuple[str, str]: ``(system_prompt, dialogue)``. Either may be empty.
319
+ """
320
+ systems: list[str] = []
321
+ turns: list[str] = []
322
+ non_system_roles = 0
323
+ for msg in messages:
324
+ role = msg.get("role", "user")
325
+ content = _flatten_content(msg.get("content"))
326
+ tool_calls = msg.get("tool_calls") if role == "assistant" else None
327
+ if not content and not tool_calls:
328
+ continue
329
+ if role == "system":
330
+ systems.append(content)
331
+ continue
332
+ non_system_roles += 1
333
+ if role == "assistant":
334
+ # When the assistant message carries tool_calls (content is
335
+ # typically None), serialise them as breadcrumbs so the CLI's
336
+ # next-turn context shows which tools were invoked. This is
337
+ # required for multi-turn tool loops to converge.
338
+ if tool_calls:
339
+ breadcrumbs = []
340
+ for tc in tool_calls:
341
+ name = _tool_call_attr(tc, "name") or "?"
342
+ args = _tool_call_attr(tc, "arguments") or "{}"
343
+ breadcrumbs.append(f"called {name} with {args}")
344
+ prefix = "; ".join(breadcrumbs)
345
+ if content:
346
+ turns.append(f"Assistant: {content}\n[tools: {prefix}]")
347
+ else:
348
+ turns.append(f"Assistant: [tools: {prefix}]")
349
+ else:
350
+ turns.append(f"Assistant: {content}")
351
+ elif role == "tool":
352
+ tcid = msg.get("tool_call_id") or "?"
353
+ turns.append(f"Tool[{tcid}]: {content}")
354
+ else:
355
+ turns.append(f"User: {content}")
356
+ if non_system_roles > 1:
357
+ _warn_multiturn_once()
358
+ return "\n\n".join(systems), "\n\n".join(turns)
359
+
360
+
361
+ def _tool_call_attr(tc: Any, attr: str) -> str | None:
362
+ """Read a tool-call field from either a LiteLLM object or a plain dict.
363
+
364
+ ``tool_calls`` entries may be ``ChatCompletionMessageToolCall`` objects
365
+ (each carrying a ``.function`` with ``.name`` / ``.arguments``) or
366
+ raw dicts (``{"function": {"name": ..., "arguments": ...}}``). Walk
367
+ both shapes.
368
+
369
+ Args:
370
+ tc: A single ``tool_calls`` entry (object or dict).
371
+ attr: The attribute to read (``"name"`` or ``"arguments"``).
372
+
373
+ Returns:
374
+ str | None: The attribute's string value, or ``None`` if absent.
375
+ """
376
+ if isinstance(tc, dict):
377
+ fn = tc.get("function") or {}
378
+ value = fn.get(attr) if isinstance(fn, dict) else getattr(fn, attr, None)
379
+ else:
380
+ fn = getattr(tc, "function", None)
381
+ value = getattr(fn, attr, None) if fn is not None else None
382
+ if value is None:
383
+ return None
384
+ return value if isinstance(value, str) else json.dumps(value)
385
+
386
+
387
+ def _run_cli_stream(
388
+ cli_path: str,
389
+ system_prompt: str,
390
+ dialogue: str,
391
+ timeout_seconds: int,
392
+ ) -> ParseResult:
393
+ """Invoke the active host CLI and return a ParseResult.
394
+
395
+ Args:
396
+ cli_path (str): Path to the host executable or compatibility wrapper.
397
+ system_prompt (str): Combined system prompt to append (may be empty).
398
+ dialogue (str): Flattened user/assistant dialogue sent on stdin.
399
+ timeout_seconds (int): Subprocess timeout.
400
+
401
+ Returns:
402
+ ParseResult: Aggregated state of the stream — success flag, terminal
403
+ text, retry errors observed, and stderr.
404
+
405
+ Raises:
406
+ ClaudeCodeCLIError: On timeout or missing binary.
407
+ """
408
+ if (
409
+ _host() == _HOST_CODEX
410
+ and Path(cli_path).name not in _CODEX_COMPAT_SCRIPT_NAME_SET
411
+ ):
412
+ return _run_codex_stream(
413
+ codex_path=cli_path,
414
+ system_prompt=system_prompt,
415
+ dialogue=dialogue,
416
+ timeout_seconds=timeout_seconds,
417
+ )
418
+ return _run_claude_stream(
419
+ cli_path=cli_path,
420
+ system_prompt=system_prompt,
421
+ dialogue=dialogue,
422
+ timeout_seconds=timeout_seconds,
423
+ )
424
+
425
+
426
+ def _run_claude_stream(
427
+ *,
428
+ cli_path: str,
429
+ system_prompt: str,
430
+ dialogue: str,
431
+ timeout_seconds: int,
432
+ ) -> ParseResult:
433
+ """Invoke ``claude -p --output-format stream-json`` and return a ParseResult."""
434
+ model = os.environ.get(_ENV_MODEL) or _DEFAULT_CLI_MODEL
435
+ cmd = [
436
+ cli_path,
437
+ "-p",
438
+ "--output-format",
439
+ "stream-json",
440
+ "--verbose",
441
+ "--include-partial-messages",
442
+ "--model",
443
+ model,
444
+ ]
445
+ if system_prompt:
446
+ cmd.extend(["--append-system-prompt", system_prompt])
447
+
448
+ # Tag the child process so any hooks it fires (e.g. claude-smart's
449
+ # Stop hook) can detect that this is a reflexio-internal invocation
450
+ # and skip publishing — otherwise extractor system prompts get
451
+ # re-published as user interactions and contaminate the corpus.
452
+ #
453
+ # CLAUDE_CODE_MAX_RETRIES=3 keeps short infrastructure blips tolerated
454
+ # while bounding the worst-case stall to a few seconds before we
455
+ # surface the failure to reflexio's stall_state table.
456
+ env = os.environ.copy()
457
+ env["CLAUDE_SMART_INTERNAL"] = "1"
458
+ env["CLAUDE_CODE_MAX_RETRIES"] = "3"
459
+
460
+ try:
461
+ proc = subprocess.run( # noqa: S603 — cmd is constructed from validated parts.
462
+ cmd,
463
+ input=dialogue,
464
+ capture_output=True,
465
+ text=True,
466
+ timeout=timeout_seconds,
467
+ check=False,
468
+ env=env,
469
+ )
470
+ except subprocess.TimeoutExpired as exc:
471
+ raise ClaudeCodeCLIError(
472
+ f"claude CLI timed out after {timeout_seconds}s"
473
+ ) from exc
474
+ except FileNotFoundError as exc:
475
+ raise ClaudeCodeCLIError(f"claude CLI not found at {cli_path}") from exc
476
+
477
+ return parse_stream_json(
478
+ proc.stdout, exit_code=proc.returncode, stderr_text=proc.stderr
479
+ )
480
+
481
+
482
+ def _run_codex_stream(
483
+ *,
484
+ codex_path: str,
485
+ system_prompt: str,
486
+ dialogue: str,
487
+ timeout_seconds: int,
488
+ ) -> ParseResult:
489
+ """Invoke ``codex exec`` and shape its output like a terminal stream result."""
490
+ output_path = _temporary_output_path()
491
+ cmd = [
492
+ codex_path,
493
+ "exec",
494
+ "--sandbox",
495
+ "read-only",
496
+ "--skip-git-repo-check",
497
+ "--ephemeral",
498
+ "--ignore-rules",
499
+ "--output-last-message",
500
+ str(output_path),
501
+ "-",
502
+ ]
503
+
504
+ env = os.environ.copy()
505
+ env[_ENV_HOST] = _HOST_CODEX
506
+ env["CLAUDE_SMART_INTERNAL"] = "1"
507
+
508
+ try:
509
+ proc = subprocess.run( # noqa: S603 — cmd is constructed from validated parts.
510
+ cmd,
511
+ input=_codex_prompt(prompt=dialogue, system_prompt=system_prompt),
512
+ capture_output=True,
513
+ text=True,
514
+ timeout=timeout_seconds,
515
+ check=False,
516
+ env=env,
517
+ )
518
+ try:
519
+ terminal_text = output_path.read_text(encoding="utf-8").strip()
520
+ except OSError:
521
+ terminal_text = ""
522
+ except subprocess.TimeoutExpired as exc:
523
+ raise ClaudeCodeCLIError(
524
+ f"codex CLI timed out after {timeout_seconds}s"
525
+ ) from exc
526
+ except FileNotFoundError as exc:
527
+ raise ClaudeCodeCLIError(f"codex CLI not found at {codex_path}") from exc
528
+ finally:
529
+ with suppress(OSError):
530
+ output_path.unlink()
531
+
532
+ return ParseResult(
533
+ success=proc.returncode == 0 and bool(terminal_text),
534
+ terminal_text=terminal_text,
535
+ stderr_text=proc.stderr,
536
+ raw_lines_parsed=1 if terminal_text else 0,
537
+ )
538
+
539
+
540
+ def _temporary_output_path() -> Path:
541
+ with tempfile.NamedTemporaryFile(
542
+ prefix="claude-smart-codex-", delete=False
543
+ ) as handle:
544
+ return Path(handle.name)
545
+
546
+
547
+ def _codex_prompt(*, prompt: str, system_prompt: str) -> str:
548
+ if not system_prompt:
549
+ return prompt
550
+ return f"{system_prompt}\n\n## Task\n{prompt}"
551
+
552
+
553
+ def _build_model_response(
554
+ model: str,
555
+ terminal_text: str,
556
+ elapsed_seconds: float,
557
+ ) -> ModelResponse:
558
+ """Wrap the CLI's terminal text in a LiteLLM ``ModelResponse``.
559
+
560
+ The stream-json transport does not surface usage tokens at the terminal
561
+ event, so prompt/completion counts are reported as zero. Downstream
562
+ LiteLLM callers tolerate this (usage is informational, not load-bearing).
563
+
564
+ Args:
565
+ model (str): The model string originally requested
566
+ (e.g. ``claude-code/default``).
567
+ terminal_text (str): The terminal ``result`` text from the CLI.
568
+ elapsed_seconds (float): Wall time the subprocess took — for logging only.
569
+
570
+ Returns:
571
+ ModelResponse: Shaped to match what callers of ``litellm.completion`` expect.
572
+ """
573
+ message = Message(role="assistant", content=terminal_text)
574
+ choice = Choices(index=0, message=message, finish_reason="stop")
575
+ usage = Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
576
+ response = ModelResponse(
577
+ id=f"claude-code-{int(time.time())}",
578
+ choices=[choice],
579
+ created=int(time.time()),
580
+ model=model,
581
+ object="chat.completion",
582
+ usage=usage,
583
+ )
584
+ _LOGGER.debug(
585
+ "claude-code provider: model=%s elapsed=%.2fs",
586
+ model,
587
+ elapsed_seconds,
588
+ )
589
+ return response
590
+
591
+
592
+ _TOOL_USE_INSTRUCTION_TEMPLATE = (
593
+ "## EXTERNAL TOOL-CALLING MODE\n"
594
+ "\n"
595
+ "You are running as a non-interactive subprocess driven by an external "
596
+ "orchestrator. The orchestrator will execute tools on your behalf. You "
597
+ "MUST NOT use Read, Edit, Write, Bash, Glob, Grep, TodoWrite, Task, or "
598
+ "ANY of your built-in Claude Code tools. The tools listed below are "
599
+ "NOT real callable functions in this session — they are the external "
600
+ "tools the ORCHESTRATOR exposes, which YOU describe by emitting a "
601
+ "structured JSON request as your final text response.\n"
602
+ "\n"
603
+ "Your response MUST be EXACTLY one JSON object on a single line, with "
604
+ "this shape:\n"
605
+ '{{"tool": "<tool_name>", "args": {{...}} }}\n'
606
+ "\n"
607
+ "Hard rules — failing any of these will break the orchestrator:\n"
608
+ "1. Output ONLY the JSON object as plain text. No prose before or after.\n"
609
+ "2. No markdown. No ```json``` code fences.\n"
610
+ "3. Do NOT invoke any built-in tool. Do NOT search the codebase. Just "
611
+ " emit the JSON.\n"
612
+ "4. ``tool`` must be exactly one of the names listed below.\n"
613
+ "5. ``args`` must be a JSON object matching that tool's parameters.\n"
614
+ "6. To terminate the orchestrator's loop, call the tool named "
615
+ "``{finish}`` with the appropriate args.\n"
616
+ "\n"
617
+ "Available orchestrator tools (each described by name + params schema):\n"
618
+ "{tool_specs}\n"
619
+ "\n"
620
+ "Now decide which tool to call and output ONLY the JSON object.\n"
621
+ )
622
+
623
+
624
+ def _render_tools_instruction(tools: list[Any], finish_tool: str = "finish") -> str:
625
+ """Render a LiteLLM tools spec into a system-prompt addendum.
626
+
627
+ Args:
628
+ tools: LiteLLM-style tools list. Each entry is a dict with
629
+ ``{"type": "function", "function": {"name", "description", "parameters"}}``.
630
+ finish_tool: The conventional finish tool name (echoed in the
631
+ instructions so the model has a recognised termination signal).
632
+
633
+ Returns:
634
+ str: The system-prompt block to append.
635
+ """
636
+ lines: list[str] = []
637
+ for tool in tools or []:
638
+ if not isinstance(tool, dict):
639
+ continue
640
+ raw_fn = tool.get("function")
641
+ fn: dict[str, Any] = raw_fn if isinstance(raw_fn, dict) else {}
642
+ name = fn.get("name") or "?"
643
+ desc = fn.get("description") or ""
644
+ params = fn.get("parameters") or {}
645
+ lines.append(
646
+ f"- {name}: {desc}\n parameters: {json.dumps(params, separators=(',', ':'))}"
647
+ )
648
+ tool_specs = "\n".join(lines) if lines else "(no tools)"
649
+ return _TOOL_USE_INSTRUCTION_TEMPLATE.format(
650
+ finish=finish_tool, tool_specs=tool_specs
651
+ )
652
+
653
+
654
+ def _maybe_append_tools_instruction(system_prompt: str, tools: list[Any] | None) -> str:
655
+ """Append the tool-use instructions when tools are present.
656
+
657
+ Args:
658
+ system_prompt: Existing system prompt (possibly empty).
659
+ tools: LiteLLM-style tools list, or None/empty.
660
+
661
+ Returns:
662
+ str: The (possibly-augmented) system prompt.
663
+ """
664
+ if not tools:
665
+ return system_prompt
666
+ instruction = _render_tools_instruction(tools)
667
+ if system_prompt:
668
+ return f"{system_prompt}\n\n{instruction}"
669
+ return instruction
670
+
671
+
672
+ def _parse_tool_use(text: str, tool_names: set[str]) -> dict[str, Any] | None:
673
+ """Try to parse a ``{"tool": ..., "args": ...}`` block from claude's output.
674
+
675
+ Walks candidate JSON substrings (plain, code-fenced, first balanced
676
+ ``{...}``) in order, returning the first that parses to a dict with a
677
+ recognised ``tool`` name and a dict ``args``.
678
+
679
+ Args:
680
+ text: Raw text from claude's ``-p --output-format json`` ``result``.
681
+ tool_names: Set of tool names registered by the caller. The returned
682
+ ``tool`` must be in this set.
683
+
684
+ Returns:
685
+ dict | None: ``{"name": str, "args": dict}`` on success; ``None`` if
686
+ no valid tool-use JSON could be located.
687
+ """
688
+ stripped = (text or "").strip()
689
+ if not stripped:
690
+ return None
691
+ candidates: list[str] = [stripped]
692
+ fence = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", stripped, re.DOTALL)
693
+ if fence:
694
+ candidates.append(fence.group(1))
695
+ # Find the first balanced ``{...}`` object via JSONDecoder.raw_decode
696
+ # instead of the greedy ``r"\{.*\}"`` regex. The greedy form swallows
697
+ # any trailing ``{...}`` on the same line and silently misses a valid
698
+ # tool-call when claude appends explanatory text after the JSON.
699
+ decoder = json.JSONDecoder()
700
+ for idx, ch in enumerate(stripped):
701
+ if ch != "{":
702
+ continue
703
+ try:
704
+ obj, end = decoder.raw_decode(stripped[idx:])
705
+ except json.JSONDecodeError:
706
+ continue
707
+ if isinstance(obj, dict):
708
+ candidates.append(stripped[idx : idx + end])
709
+ break
710
+ for candidate in candidates:
711
+ try:
712
+ parsed = json.loads(candidate)
713
+ except json.JSONDecodeError:
714
+ continue
715
+ if not isinstance(parsed, dict):
716
+ continue
717
+ name = parsed.get("tool")
718
+ args = parsed.get("args")
719
+ if name in tool_names and isinstance(args, dict):
720
+ return {"name": name, "args": args}
721
+ return None
722
+
723
+
724
+ def _build_model_response_with_tool_call(
725
+ *,
726
+ model: str,
727
+ terminal_text: str,
728
+ elapsed_seconds: float,
729
+ tool_use: dict[str, Any],
730
+ ) -> ModelResponse:
731
+ """Wrap the CLI terminal text as a ``ModelResponse`` carrying one ``tool_calls`` entry.
732
+
733
+ The stream-json transport does not surface usage tokens at the terminal
734
+ event, so prompt/completion counts are reported as zero (same convention
735
+ as :func:`_build_model_response`). Downstream LiteLLM callers tolerate
736
+ this — usage is informational, not load-bearing.
737
+
738
+ Args:
739
+ model: Model string passed in by LiteLLM.
740
+ terminal_text: The terminal ``result`` text from the CLI (retained
741
+ for signature parity with the plain-text branch; surfaced via
742
+ logging only).
743
+ elapsed_seconds: Subprocess wall time, for logging only.
744
+ tool_use: Parsed ``{"name": str, "args": dict}`` from the model output.
745
+
746
+ Returns:
747
+ ModelResponse: A LiteLLM response with ``choices[0].message.tool_calls`` set
748
+ and ``content`` set to ``None`` — matches OpenAI/Anthropic tool-call shape.
749
+ """
750
+ del terminal_text # retained for signature parity only
751
+ call_id = f"call_{int(time.time() * 1000)}"
752
+ tool_call = ChatCompletionMessageToolCall(
753
+ id=call_id,
754
+ type="function",
755
+ function=Function(
756
+ name=tool_use["name"],
757
+ arguments=json.dumps(tool_use["args"]),
758
+ ),
759
+ )
760
+ message = Message(role="assistant", content=None, tool_calls=[tool_call])
761
+ choice = Choices(index=0, message=message, finish_reason="tool_calls")
762
+ usage = Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
763
+ response = ModelResponse(
764
+ id=f"claude-code-{int(time.time())}",
765
+ choices=[choice],
766
+ created=int(time.time()),
767
+ model=model,
768
+ object="chat.completion",
769
+ usage=usage,
770
+ )
771
+ _LOGGER.debug(
772
+ "claude-code provider: tool_call name=%s elapsed=%.2fs",
773
+ tool_use["name"],
774
+ elapsed_seconds,
775
+ )
776
+ return response
777
+
778
+
779
+ def _maybe_append_schema(system_prompt: str, response_format: Any) -> str:
780
+ """Return *system_prompt* extended with a JSON-schema instruction when applicable.
781
+
782
+ Args:
783
+ system_prompt: Existing system prompt (possibly empty).
784
+ response_format: The LiteLLM response_format value, if any.
785
+
786
+ Returns:
787
+ str: The (possibly-augmented) system prompt.
788
+ """
789
+ instruction = _schema_instruction(response_format)
790
+ if not instruction:
791
+ return system_prompt
792
+ if system_prompt:
793
+ return f"{system_prompt}\n\n{instruction}"
794
+ return instruction
795
+
796
+
797
+ _IGNORED_PARAMS: tuple[str, ...] = (
798
+ "temperature",
799
+ "max_tokens",
800
+ "top_p",
801
+ "stop",
802
+ "seed",
803
+ "frequency_penalty",
804
+ "presence_penalty",
805
+ )
806
+
807
+
808
+ def _warn_on_ignored_params(*sources: Any) -> None:
809
+ """Emit one-time warnings for LiteLLM params the CLI cannot honour.
810
+
811
+ Args:
812
+ *sources: Any number of dict-like sources (kwargs, ``optional_params``).
813
+
814
+ Returns:
815
+ None
816
+ """
817
+ for source in sources:
818
+ if not isinstance(source, dict):
819
+ continue
820
+ for name in _IGNORED_PARAMS:
821
+ if source.get(name) is not None:
822
+ _warn_unsupported_param_once(name)
823
+
824
+
825
+ class ClaudeCodeLLM(CustomLLM):
826
+ """LiteLLM custom handler routing completions through the ``claude`` CLI."""
827
+
828
+ def __init__(
829
+ self,
830
+ cli_path: str | None = None,
831
+ timeout_seconds: int | None = None,
832
+ storage: Any | None = None,
833
+ ):
834
+ """Initialise the handler.
835
+
836
+ Args:
837
+ cli_path (str | None): Override for the ``claude`` binary path.
838
+ timeout_seconds (int | None): Override for subprocess timeout.
839
+ storage (Any | None): A BaseStorage-shaped object used to persist
840
+ stall_state on credit/auth failures. Typed as ``Any`` to avoid
841
+ a circular import with ``server.services.storage``. When None,
842
+ stall state is not recorded (back-compat).
843
+ """
844
+ super().__init__()
845
+ self._explicit_cli_path = cli_path
846
+ self._explicit_timeout = timeout_seconds
847
+ self._storage = storage
848
+
849
+ def _cli_path(self) -> str:
850
+ """Resolve the CLI path, raising when unavailable.
851
+
852
+ Returns:
853
+ str: Absolute path to the ``claude`` executable.
854
+
855
+ Raises:
856
+ ClaudeCodeCLIError: If the CLI cannot be located.
857
+ """
858
+ path = self._explicit_cli_path or _resolve_cli_path()
859
+ if not path:
860
+ raise ClaudeCodeCLIError(
861
+ f"{_cli_name()} CLI not found for {_host()}. Install the host "
862
+ f"CLI or set {_ENV_CLI_PATH} to an executable path."
863
+ )
864
+ return path
865
+
866
+ def _timeout(self) -> int:
867
+ """Resolve the subprocess timeout.
868
+
869
+ Returns:
870
+ int: Timeout in seconds.
871
+ """
872
+ if self._explicit_timeout is not None:
873
+ return self._explicit_timeout
874
+ raw = os.environ.get(_ENV_TIMEOUT)
875
+ if raw:
876
+ try:
877
+ return max(1, int(raw))
878
+ except ValueError:
879
+ _LOGGER.warning("Ignoring non-integer %s=%r", _ENV_TIMEOUT, raw)
880
+ return _DEFAULT_TIMEOUT_SECONDS
881
+
882
+ def completion( # type: ignore[override]
883
+ self,
884
+ *args: Any,
885
+ model: str = "claude-code/default",
886
+ messages: list[dict[str, Any]] | None = None,
887
+ optional_params: dict[str, Any] | None = None,
888
+ **kwargs: Any,
889
+ ) -> ModelResponse:
890
+ """Execute a completion via the ``claude`` CLI.
891
+
892
+ Args:
893
+ *args: Ignored; LiteLLM may pass ``model_response`` positionally.
894
+ model: The requested model slug (e.g. ``claude-code/default``).
895
+ messages: LiteLLM-style chat messages.
896
+ optional_params: LiteLLM's bag of generation params — checked for
897
+ ``response_format`` and logged when unsupported knobs are set.
898
+ **kwargs: Other LiteLLM arguments (api_key, logging_obj, etc.);
899
+ ignored by this handler.
900
+
901
+ Returns:
902
+ ModelResponse: Shaped to match what ``litellm.completion`` callers expect.
903
+
904
+ Raises:
905
+ ClaudeCodeCLIError: On CLI failure or missing binary.
906
+ """
907
+ del args
908
+ messages = messages or []
909
+ optional_params = optional_params or {}
910
+
911
+ # Tools may arrive in either ``optional_params["tools"]`` (LiteLLM's
912
+ # standard plumbing for custom providers) or as a top-level ``tools``
913
+ # kwarg. Read both before discarding kwargs.
914
+ tools = optional_params.get("tools") or kwargs.get("tools")
915
+ del kwargs
916
+
917
+ _warn_on_ignored_params(optional_params)
918
+
919
+ response_format = optional_params.get("response_format")
920
+ system_prompt, dialogue = _split_system_and_dialogue(messages)
921
+ # When ``tools`` is present, render the tools spec into the system
922
+ # prompt and ignore response_format (mutually exclusive with our
923
+ # tool_use JSON output contract).
924
+ if tools:
925
+ system_prompt = _maybe_append_tools_instruction(system_prompt, tools)
926
+ else:
927
+ system_prompt = _maybe_append_schema(system_prompt, response_format)
928
+
929
+ started = time.perf_counter()
930
+ result = _run_cli_stream(
931
+ cli_path=self._cli_path(),
932
+ system_prompt=system_prompt,
933
+ dialogue=dialogue,
934
+ timeout_seconds=self._timeout(),
935
+ )
936
+
937
+ if result.success:
938
+ self._clear_stall_safely()
939
+ elapsed = time.perf_counter() - started
940
+
941
+ # When ``tools`` are provided, attempt to parse the model's
942
+ # terminal text as a ``{"tool": ..., "args": ...}`` JSON object.
943
+ # If parsing succeeds, return a tool-call ModelResponse; otherwise
944
+ # warn (so the silent fall-through is observable) and return a
945
+ # plain-text response, which the caller treats as "no tool_calls"
946
+ # and uses to terminate the tool loop.
947
+ if tools:
948
+ tool_names: set[str] = {
949
+ name
950
+ for tool in tools
951
+ if isinstance(tool, dict)
952
+ and isinstance(
953
+ name := (tool.get("function") or {}).get("name"), str
954
+ )
955
+ }
956
+ tool_use = _parse_tool_use(result.terminal_text, tool_names)
957
+ if tool_use is not None:
958
+ return _build_model_response_with_tool_call(
959
+ model=model,
960
+ terminal_text=result.terminal_text,
961
+ elapsed_seconds=elapsed,
962
+ tool_use=tool_use,
963
+ )
964
+ # Log a metadata-only warning (no raw payload) — the model
965
+ # output can carry user content / source code; deferring the
966
+ # body to a DEBUG fingerprint avoids turning a recoverable
967
+ # parse miss into a log-retention concern.
968
+ _LOGGER.warning(
969
+ "claude-code provider: tools=%s were provided but no valid "
970
+ "tool_use JSON was parsed from model output; the tool loop "
971
+ "will terminate without a finish_tool call.",
972
+ sorted(tool_names),
973
+ )
974
+ _LOGGER.debug(
975
+ "claude-code provider: unparsable tool-use payload length=%d",
976
+ len(result.terminal_text),
977
+ )
978
+
979
+ return _build_model_response(
980
+ model=model,
981
+ terminal_text=result.terminal_text,
982
+ elapsed_seconds=elapsed,
983
+ )
984
+
985
+ self._record_stall_safely(result)
986
+ raise ClaudeCodeCLIError(
987
+ f"claude -p stream failed; retry_errors={result.retry_errors}; "
988
+ f"stderr={result.stderr_text[:200]!r}"
989
+ )
990
+
991
+ def _record_stall_safely(self, result: ParseResult) -> None:
992
+ """Persist a stall_state row on credit/auth failure. Never raises.
993
+
994
+ Args:
995
+ result (ParseResult): Output of :func:`_run_cli_stream`.
996
+
997
+ Returns:
998
+ None
999
+ """
1000
+ reason = classify_stall(result)
1001
+ if reason is None or self._storage is None:
1002
+ return
1003
+ try:
1004
+ self._storage.upsert_stall_state(
1005
+ reason=reason,
1006
+ stalled_at=datetime.now(UTC),
1007
+ reset_estimate=parse_reset_estimate(
1008
+ f"{result.stderr_text} {result.terminal_text}"
1009
+ ),
1010
+ error_message=(result.stderr_text or result.terminal_text)[:1000],
1011
+ )
1012
+ except Exception as exc: # noqa: BLE001 — never crash the provider over telemetry.
1013
+ _LOGGER.warning("Failed to record stall_state: %s", exc)
1014
+
1015
+ def _clear_stall_safely(self) -> None:
1016
+ """Clear any prior stall_state row after a successful run. Never raises.
1017
+
1018
+ Returns:
1019
+ None
1020
+ """
1021
+ if self._storage is None:
1022
+ return
1023
+ try:
1024
+ self._storage.clear_stall_state()
1025
+ except Exception as exc: # noqa: BLE001 — never crash the provider over telemetry.
1026
+ _LOGGER.warning("Failed to clear stall_state: %s", exc)
1027
+
1028
+ async def acompletion( # type: ignore[override]
1029
+ self, *args: Any, **kwargs: Any
1030
+ ) -> ModelResponse:
1031
+ """Async entry point — delegates to the sync CLI call via ``to_thread``.
1032
+
1033
+ Args:
1034
+ *args: Forwarded to :meth:`completion`.
1035
+ **kwargs: Forwarded to :meth:`completion`.
1036
+
1037
+ Returns:
1038
+ ModelResponse: The CLI-backed completion result.
1039
+ """
1040
+ import asyncio
1041
+
1042
+ return await asyncio.to_thread(self.completion, *args, **kwargs)
1043
+
1044
+
1045
+ _REGISTERED = False
1046
+ _HANDLER: ClaudeCodeLLM | None = None
1047
+
1048
+
1049
+ def register_if_enabled(storage: Any | None = None) -> bool:
1050
+ """Register the ``claude-code`` provider with LiteLLM if enabled and available.
1051
+
1052
+ Idempotent — safe to call more than once per process. Opt-in via
1053
+ ``CLAUDE_SMART_USE_LOCAL_CLI=1``. Skips registration (with a warning)
1054
+ when the env var is set but the CLI is not on PATH.
1055
+
1056
+ Args:
1057
+ storage (Any | None): Optional BaseStorage-shaped handle used by the
1058
+ provider to persist stall_state on credit/auth failures. The
1059
+ caller (``LiteLLMClient`` import-time wiring) typically has no
1060
+ storage available, so use :func:`set_storage` to late-bind it
1061
+ once a request context exists.
1062
+
1063
+ Returns:
1064
+ bool: True if the provider is registered after this call.
1065
+ """
1066
+ global _REGISTERED, _HANDLER
1067
+ if _REGISTERED:
1068
+ if storage is not None and _HANDLER is not None:
1069
+ _HANDLER._storage = storage
1070
+ return True
1071
+ if not _env_enabled():
1072
+ return False
1073
+ cli_path = _resolve_cli_path()
1074
+ if not cli_path:
1075
+ _LOGGER.warning(
1076
+ "%s=1 is set but the %s CLI is not available for %s. "
1077
+ "Install the host CLI or set %s; skipping provider registration.",
1078
+ ENV_ENABLE,
1079
+ _cli_name(),
1080
+ _host(),
1081
+ _ENV_CLI_PATH,
1082
+ )
1083
+ return False
1084
+
1085
+ existing = list(getattr(litellm, "custom_provider_map", None) or [])
1086
+ if any(entry.get("provider") == PROVIDER_KEY for entry in existing):
1087
+ _REGISTERED = True
1088
+ return True
1089
+ _HANDLER = ClaudeCodeLLM(storage=storage)
1090
+ existing.append({"provider": PROVIDER_KEY, "custom_handler": _HANDLER})
1091
+ litellm.custom_provider_map = existing
1092
+ _REGISTERED = True
1093
+ _LOGGER.info("Registered %s LiteLLM provider (cli=%s)", PROVIDER_KEY, cli_path)
1094
+ return True
1095
+
1096
+
1097
+ def set_storage(storage: Any) -> None:
1098
+ """Bind storage onto the registered handler after registration.
1099
+
1100
+ The provider is registered at LiteLLM-import time, before any
1101
+ request-scoped storage exists. Once a storage instance is available,
1102
+ call this to enable stall_state persistence on the live handler.
1103
+
1104
+ Args:
1105
+ storage (Any): BaseStorage-shaped instance.
1106
+
1107
+ Returns:
1108
+ None
1109
+ """
1110
+ if _HANDLER is not None:
1111
+ _HANDLER._storage = storage
1112
+
1113
+
1114
+ __all__ = [
1115
+ "ENV_ENABLE",
1116
+ "PROVIDER_KEY",
1117
+ "ClaudeCodeCLIError",
1118
+ "ClaudeCodeLLM",
1119
+ "is_claude_code_available",
1120
+ "register_if_enabled",
1121
+ "set_storage",
1122
+ ]