claude-smart 0.2.44 → 0.2.46

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 (323) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +34 -5
  3. package/bin/claude-smart.js +295 -5
  4. package/package.json +22 -3
  5. package/plugin/.claude-plugin/plugin.json +4 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +22 -2
  8. package/plugin/commands/clear-all.md +1 -0
  9. package/plugin/commands/dashboard.md +1 -0
  10. package/plugin/commands/learn.md +1 -0
  11. package/plugin/commands/restart.md +1 -0
  12. package/plugin/commands/show.md +1 -0
  13. package/plugin/dashboard/app/configure/env/page.tsx +40 -14
  14. package/plugin/dashboard/app/configure/server/page.tsx +51 -1
  15. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  18. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  19. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  20. package/plugin/dashboard/lib/status.ts +10 -3
  21. package/plugin/dashboard/lib/types.ts +18 -6
  22. package/plugin/hooks/codex-hooks.json +0 -1
  23. package/plugin/opencode/assistant-buffer.ts +108 -0
  24. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  25. package/plugin/opencode/dist/internal.js +12 -0
  26. package/plugin/opencode/dist/payload.js +85 -0
  27. package/plugin/opencode/dist/server.mjs +158 -0
  28. package/plugin/opencode/internal.ts +18 -0
  29. package/plugin/opencode/package.json +3 -0
  30. package/plugin/opencode/payload.ts +90 -0
  31. package/plugin/opencode/server.mts +174 -0
  32. package/plugin/opencode/tsconfig.json +13 -0
  33. package/plugin/pyproject.toml +20 -3
  34. package/plugin/scripts/_lib.sh +77 -29
  35. package/plugin/scripts/backend-service.sh +27 -5
  36. package/plugin/scripts/dashboard-service.sh +7 -5
  37. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  38. package/plugin/scripts/hook_entry.sh +3 -3
  39. package/plugin/scripts/opencode-claude-compat +4 -0
  40. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  41. package/plugin/scripts/opencode-claude-compat.js +225 -0
  42. package/plugin/scripts/smart-install.sh +10 -8
  43. package/plugin/src/README.md +1 -1
  44. package/plugin/src/claude_smart/cli.py +304 -6
  45. package/plugin/src/claude_smart/env_config.py +12 -4
  46. package/plugin/src/claude_smart/events/session_start.py +26 -7
  47. package/plugin/src/claude_smart/events/stop.py +1 -1
  48. package/plugin/src/claude_smart/ids.py +1 -1
  49. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  50. package/plugin/src/claude_smart/runtime.py +7 -1
  51. package/plugin/uv.lock +1 -1
  52. package/plugin/vendor/reflexio/.env.example +54 -30
  53. package/plugin/vendor/reflexio/README.md +14 -8
  54. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  55. package/plugin/vendor/reflexio/reflexio/README.md +1 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  57. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  58. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
  63. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  64. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  65. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  66. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  67. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  68. package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
  69. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  70. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  71. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  142. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
  143. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  144. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
  145. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  146. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
  147. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  148. package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
  149. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
  150. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  151. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  152. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  153. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  157. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  158. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
  159. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  160. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  161. package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
  162. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  163. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  164. package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
  165. package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
  166. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  167. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
  168. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  169. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  170. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  171. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  172. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  173. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  174. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  175. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  176. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  177. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  178. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  179. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  180. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  181. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  182. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  183. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  184. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  185. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  186. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  187. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  188. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
  189. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  197. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  198. package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
  199. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  200. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  201. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  202. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  203. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  204. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  205. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  206. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  207. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  208. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  209. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  211. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  212. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  214. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  215. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  216. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  217. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  218. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  219. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  220. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  221. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
  222. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  223. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
  225. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  226. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  227. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
  228. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  229. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
  230. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  231. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  232. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  233. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  234. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  235. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  237. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  238. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  239. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  241. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  242. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
  243. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  244. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  245. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  246. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  247. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  248. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  249. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
  250. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  251. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  252. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  253. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
  254. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  255. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  256. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  257. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
  258. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
  259. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
  260. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  261. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
  262. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
  263. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
  264. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  265. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  266. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  277. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  278. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  279. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
  282. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  283. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  284. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  285. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  286. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  287. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  288. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  289. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  290. package/scripts/setup-claude-smart.sh +8 -3
  291. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  292. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  293. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  294. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  295. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  296. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  297. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  298. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  299. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  300. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  301. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  302. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  303. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  304. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  305. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  306. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  307. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  308. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  309. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  310. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  311. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  312. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  313. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  314. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  318. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  319. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  320. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  321. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  322. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  323. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -0,0 +1,224 @@
1
+ """Render reflexio preferences + skills as markdown for display or injection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import Any
7
+
8
+ from openclaw_smart import oc_cite
9
+
10
+
11
+ def _first_nonempty(*values: Any) -> str:
12
+ """Return the first truthy string value, or an empty string."""
13
+ for v in values:
14
+ if isinstance(v, str) and v.strip():
15
+ return v.strip()
16
+ return ""
17
+
18
+
19
+ def render(
20
+ *,
21
+ project_id: str,
22
+ user_playbooks: Iterable[Any],
23
+ agent_playbooks: Iterable[Any],
24
+ profiles: Iterable[Any],
25
+ ) -> str:
26
+ """Render skills + preferences as full audit markdown.
27
+
28
+ Empty sections are omitted. When all sections are empty, returns "".
29
+ """
30
+ markdown, _ = render_with_registry(
31
+ project_id=project_id,
32
+ user_playbooks=user_playbooks,
33
+ agent_playbooks=agent_playbooks,
34
+ profiles=profiles,
35
+ )
36
+ return markdown
37
+
38
+
39
+ def render_with_registry(
40
+ *,
41
+ project_id: str,
42
+ user_playbooks: Iterable[Any],
43
+ agent_playbooks: Iterable[Any],
44
+ profiles: Iterable[Any],
45
+ ) -> tuple[str, list[dict[str, Any]]]:
46
+ """Variant of ``render`` that also returns the citation registry.
47
+
48
+ Every skill and preference bullet is tagged with a short ``[oc:ID]``
49
+ prefix. The registry maps those ids back to ``{id, kind, title,
50
+ content}`` entries so ``events.agent_end`` can resolve citations into
51
+ human-readable titles for the dashboard.
52
+
53
+ Agent playbooks (cross-project, distilled) are listed before user
54
+ playbooks (this project's lessons) under one ``### Project-specific
55
+ skills`` heading. The model doesn't need to reason about the split.
56
+ """
57
+ playbook_lines, playbook_entries = _format_combined_playbooks(
58
+ agent_playbooks=agent_playbooks, user_playbooks=user_playbooks
59
+ )
60
+ profile_lines, profile_entries = _format_profiles(profiles)
61
+ if not playbook_lines and not profile_lines:
62
+ return "", []
63
+
64
+ sections: list[str] = [f"## openclaw-smart — project `{project_id}`"]
65
+ if playbook_lines:
66
+ sections.append("### Project-specific skills")
67
+ sections.extend(playbook_lines)
68
+ if profile_lines:
69
+ sections.append("### Project preferences")
70
+ sections.extend(profile_lines)
71
+ sections.append(oc_cite.CITATION_INSTRUCTION)
72
+ return "\n".join(sections) + "\n", playbook_entries + profile_entries
73
+
74
+
75
+ def render_inline(
76
+ *,
77
+ project_id: str,
78
+ user_playbooks: Iterable[Any],
79
+ agent_playbooks: Iterable[Any],
80
+ profiles: Iterable[Any],
81
+ ) -> str:
82
+ """Render skills + preferences for mid-session injection.
83
+
84
+ Same bullet format as ``render`` but with no top-level project header.
85
+ This block is injected just-in-time alongside an in-flight user prompt or
86
+ tool call, so the caller already has project context.
87
+ """
88
+ markdown, _ = render_inline_with_registry(
89
+ project_id=project_id,
90
+ user_playbooks=user_playbooks,
91
+ agent_playbooks=agent_playbooks,
92
+ profiles=profiles,
93
+ )
94
+ return markdown
95
+
96
+
97
+ def render_inline_with_registry(
98
+ *,
99
+ project_id: str,
100
+ user_playbooks: Iterable[Any],
101
+ agent_playbooks: Iterable[Any],
102
+ profiles: Iterable[Any],
103
+ ) -> tuple[str, list[dict[str, Any]]]:
104
+ """Variant of ``render_inline`` that also returns the citation registry."""
105
+ del project_id # kept for symmetry with ``render_with_registry``.
106
+ playbook_lines, playbook_entries = _format_combined_playbooks(
107
+ agent_playbooks=agent_playbooks, user_playbooks=user_playbooks
108
+ )
109
+ profile_lines, profile_entries = _format_profiles(profiles)
110
+ if not playbook_lines and not profile_lines:
111
+ return "", []
112
+ sections: list[str] = []
113
+ if playbook_lines:
114
+ sections.append("### Relevant project-specific skills")
115
+ sections.extend(playbook_lines)
116
+ if profile_lines:
117
+ sections.append("### Relevant project preferences")
118
+ sections.extend(profile_lines)
119
+ sections.append(oc_cite.CITATION_INSTRUCTION)
120
+ return "\n".join(sections) + "\n", playbook_entries + profile_entries
121
+
122
+
123
+ def _format_combined_playbooks(
124
+ *,
125
+ agent_playbooks: Iterable[Any],
126
+ user_playbooks: Iterable[Any],
127
+ ) -> tuple[list[str], list[dict[str, Any]]]:
128
+ """Render agent playbooks first, then user playbooks, with one shared rank counter."""
129
+ lines: list[str] = []
130
+ entries: list[dict[str, Any]] = []
131
+ rank = 0
132
+ for pb in agent_playbooks:
133
+ rank = _append_playbook_bullet(
134
+ pb, "agent_playbook_id", "agent_playbook", rank, lines, entries
135
+ )
136
+ for pb in user_playbooks:
137
+ rank = _append_playbook_bullet(
138
+ pb, "user_playbook_id", "user_playbook", rank, lines, entries
139
+ )
140
+ return lines, entries
141
+
142
+
143
+ def _append_playbook_bullet(
144
+ pb: Any,
145
+ id_field: str,
146
+ source_kind: str,
147
+ rank: int,
148
+ lines: list[str],
149
+ entries: list[dict[str, Any]],
150
+ ) -> int:
151
+ content = _first_nonempty(_field(pb, "content"))
152
+ if not content:
153
+ return rank
154
+ rank += 1
155
+ trigger = _first_nonempty(_field(pb, "trigger"))
156
+ rationale = _first_nonempty(_field(pb, "rationale"))
157
+ real_id = _field(pb, id_field)
158
+ item_id = oc_cite.rank_id("playbook", rank, real_id)
159
+ title = _title_from_content(content)
160
+ bullet = f"- [oc:{item_id}] {content}"
161
+ if trigger:
162
+ bullet += f" _(when: {trigger})_"
163
+ if rationale:
164
+ bullet += f" — *why:* {rationale}"
165
+ lines.append(bullet)
166
+ entries.append(
167
+ {
168
+ "id": item_id,
169
+ "kind": "playbook",
170
+ "title": title,
171
+ "content": content,
172
+ "real_id": str(real_id) if real_id is not None else None,
173
+ "source_kind": source_kind,
174
+ }
175
+ )
176
+ return rank
177
+
178
+
179
+ def _format_profiles(
180
+ profiles: Iterable[Any],
181
+ ) -> tuple[list[str], list[dict[str, Any]]]:
182
+ lines: list[str] = []
183
+ entries: list[dict[str, Any]] = []
184
+ rank = 0
185
+ for p in profiles:
186
+ content = _first_nonempty(_field(p, "content"))
187
+ if not content:
188
+ continue
189
+ rank += 1
190
+ real_id = _field(p, "profile_id")
191
+ item_id = oc_cite.rank_id("profile", rank, real_id)
192
+ title = _title_from_content(content)
193
+ lines.append(f"- [oc:{item_id}] {content}")
194
+ entries.append(
195
+ {
196
+ "id": item_id,
197
+ "kind": "profile",
198
+ "title": title,
199
+ "content": content,
200
+ "real_id": str(real_id) if real_id is not None else None,
201
+ }
202
+ )
203
+ return lines, entries
204
+
205
+
206
+ def _title_from_content(content: str, limit: int = 80) -> str:
207
+ """Derive a compact human-readable title from a bullet's content."""
208
+ text = content.strip()
209
+ if not text:
210
+ return ""
211
+ for terminator in (". ", "\n"):
212
+ idx = text.find(terminator)
213
+ if 0 < idx <= limit:
214
+ return text[:idx].rstrip()
215
+ if len(text) <= limit:
216
+ return text
217
+ return text[: limit - 1].rstrip() + "…"
218
+
219
+
220
+ def _field(obj: Any, name: str) -> Any:
221
+ """Read ``name`` from either an attribute or a dict key."""
222
+ if isinstance(obj, dict):
223
+ return obj.get(name)
224
+ return getattr(obj, name, None)
@@ -0,0 +1,79 @@
1
+ """Shared "search reflexio, render markdown, emit envelope" pipeline.
2
+
3
+ before_tool_call and before_prompt_build both (a) run a query-aware reflexio
4
+ search, (b) render the hits with ``context_format.render_inline_with_registry``,
5
+ (c) persist the citation registry for the agent_end hook to resolve, and
6
+ (d) emit a ``prependContext`` envelope on stdout. The TS shim wraps stdout
7
+ and translates it to the openClaw plugin-SDK return shape.
8
+
9
+ Per Phase 0 finding B2 we use ``prependContext`` (per-turn) rather than
10
+ ``prependSystemContext`` (cached) so injected context refreshes every turn.
11
+ ``before_tool_call`` is observe-only per finding Q1, so handlers there
12
+ should NOT call ``emit_context`` — they only append state.
13
+
14
+ The caller remains responsible for handler-specific framing — see the two
15
+ call sites for the small policy differences.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import sys
22
+ import time
23
+
24
+ from openclaw_smart import context_format, oc_cite, state
25
+ from openclaw_smart.reflexio_adapter import Adapter
26
+
27
+
28
+ def emit_context(
29
+ *,
30
+ session_id: str,
31
+ project_id: str,
32
+ query: str,
33
+ top_k: int,
34
+ adapter: Adapter | None = None,
35
+ ) -> bool:
36
+ """Search reflexio, render hits, emit ``prependContext`` JSON on stdout.
37
+
38
+ Args:
39
+ session_id (str): openClaw session id (``sessionKey``); used to scope
40
+ the per-session citation registry.
41
+ project_id (str): reflexio ``user_id`` for this repo.
42
+ query (str): Free-text query routed to reflexio's unified
43
+ ``/api/search`` endpoint, which fans out to user playbooks
44
+ (project-scoped), agent playbooks (global), and preferences
45
+ (project-scoped) server-side.
46
+ top_k (int): Cap on hits per collection.
47
+ adapter (Adapter | None): Injection seam for tests. A fresh
48
+ ``Adapter()`` is used when ``None``.
49
+
50
+ Returns:
51
+ bool: ``True`` when markdown was emitted to stdout; ``False`` when
52
+ the search returned nothing to inject.
53
+ """
54
+ user_playbooks, agent_playbooks, profiles = (adapter or Adapter()).search_all(
55
+ project_id=project_id,
56
+ query=query,
57
+ top_k=top_k,
58
+ )
59
+ markdown, registry = context_format.render_inline_with_registry(
60
+ project_id=project_id,
61
+ user_playbooks=user_playbooks,
62
+ agent_playbooks=agent_playbooks,
63
+ profiles=profiles,
64
+ )
65
+ if not markdown:
66
+ return False
67
+
68
+ oc_cite.ensure_installed()
69
+ state.append_injected(
70
+ session_id,
71
+ (dict(entry, ts=int(time.time())) for entry in registry),
72
+ )
73
+
74
+ sys.stdout.write(json.dumps({"prependContext": markdown}))
75
+ sys.stdout.write("\n")
76
+ return True
77
+
78
+
79
+ __all__ = ["emit_context"]
@@ -0,0 +1,160 @@
1
+ """after_tool_call hook — record the tool invocation and its outcome.
2
+
3
+ openClaw delivers ``after_tool_call`` payloads with camelCase fields
4
+ (``toolName``, ``params``, ``result``). Per Phase 0 finding B1 the handler
5
+ translates these to snake_case before persisting so downstream consumers
6
+ (state buffer, reflexio API) see a stable shape.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import time
13
+ from typing import Any
14
+
15
+ from openclaw_smart import state
16
+
17
+ # Tool inputs are persisted locally and later published to reflexio, so we
18
+ # apply a conservative redaction pass at ingestion time. Chosen to avoid
19
+ # false positives over maximal coverage — the dashboard shows these
20
+ # verbatim, and users noticing a masked command is far less surprising
21
+ # than a masked ``LOG_LEVEL=INFO``.
22
+ _MAX_STR_LEN = 4096
23
+ _SECRET_ASSIGNMENT = re.compile(
24
+ r"(?P<key>[A-Z][A-Z0-9_]{2,})=(?P<quote>['\"]?)"
25
+ r"(?P<value>[A-Za-z0-9+/=_\-]{20,})(?P=quote)"
26
+ )
27
+
28
+
29
+ def _looks_like_secret(value: str) -> bool:
30
+ """Heuristic: mixed-case letters plus digits suggest a high-entropy token."""
31
+ has_lower = any(c.islower() for c in value)
32
+ has_upper = any(c.isupper() for c in value)
33
+ has_digit = any(c.isdigit() for c in value)
34
+ return has_lower and has_upper and has_digit
35
+
36
+
37
+ def _mask_secrets(text: str) -> str:
38
+ """Mask values that look like high-entropy secrets in ``KEY=value`` form."""
39
+
40
+ def sub(match: re.Match[str]) -> str:
41
+ value = match.group("value")
42
+ if not _looks_like_secret(value):
43
+ return match.group(0)
44
+ key = match.group("key")
45
+ quote = match.group("quote")
46
+ return f"{key}={quote}<redacted:{len(value)}>{quote}"
47
+
48
+ return _SECRET_ASSIGNMENT.sub(sub, text)
49
+
50
+
51
+ def _redact_string(value: str) -> str:
52
+ """Mask secrets in ``value`` then truncate to ``_MAX_STR_LEN`` chars."""
53
+ masked = _mask_secrets(value)
54
+ if len(masked) > _MAX_STR_LEN:
55
+ return masked[:_MAX_STR_LEN] + "…(truncated)"
56
+ return masked
57
+
58
+
59
+ def _redact(tool_input: dict[str, Any]) -> dict[str, Any]:
60
+ """Redact obvious secrets and truncate oversized string values."""
61
+ return {
62
+ k: _redact_string(v) if isinstance(v, str) else v for k, v in tool_input.items()
63
+ }
64
+
65
+
66
+ def _derive_status(tool_response: Any) -> str:
67
+ """Classify the tool outcome as 'success' or 'error'.
68
+
69
+ openClaw's ``after_tool_call`` payload places the tool result under
70
+ ``result``; a structured failure may use ``is_error``/``error`` keys,
71
+ while plain string results default to success.
72
+ """
73
+ if isinstance(tool_response, dict) and (
74
+ tool_response.get("is_error") or tool_response.get("error")
75
+ ):
76
+ return "error"
77
+ return "success"
78
+
79
+
80
+ _OUTPUT_TEXT_KEYS = ("stdout", "stderr", "output", "content", "text", "error")
81
+
82
+
83
+ def _flatten_tool_response_text(tool_response: Any) -> str:
84
+ """Flatten ``tool_response`` into a single string for buffering.
85
+
86
+ Tool responses arrive in heterogeneous shapes — Bash sends a dict with
87
+ ``stdout``/``stderr``, Edit/Read send a string or a dict with
88
+ ``content``/``output``, and failures populate ``error``. Joining the
89
+ well-known string-valued keys preserves the parts most useful for
90
+ downstream learning (failure messages, command output) without
91
+ serializing entire structured payloads.
92
+ """
93
+ if tool_response is None:
94
+ return ""
95
+ if isinstance(tool_response, str):
96
+ return tool_response
97
+ if isinstance(tool_response, dict):
98
+ parts = [
99
+ tool_response[key]
100
+ for key in _OUTPUT_TEXT_KEYS
101
+ if isinstance(tool_response.get(key), str) and tool_response[key]
102
+ ]
103
+ if parts:
104
+ return "\n".join(parts)
105
+ for key in ("text", "content"):
106
+ value = tool_response.get(key)
107
+ if isinstance(value, str):
108
+ return value
109
+ return ""
110
+ for attr in ("text", "content"):
111
+ value = getattr(tool_response, attr, None)
112
+ if isinstance(value, str):
113
+ return value
114
+ return ""
115
+
116
+
117
+ def _extract_fields(payload: dict[str, Any]) -> dict[str, Any]:
118
+ """Map openClaw's camelCase keys onto our snake_case schema.
119
+
120
+ Accepts either style so the handler is resilient to dispatcher changes —
121
+ if a future TS shim emits already-translated keys, the snake_case
122
+ branch wins.
123
+ """
124
+ return {
125
+ "session_id": payload.get("sessionKey") or payload.get("sessionId"),
126
+ "tool_name": payload.get("tool_name") or payload.get("toolName") or "",
127
+ "tool_input": payload.get("tool_input") or payload.get("params") or {},
128
+ "tool_response": payload.get("tool_response")
129
+ if "tool_response" in payload
130
+ else payload.get("result"),
131
+ }
132
+
133
+
134
+ def handle(payload: dict[str, Any]) -> None:
135
+ """Persist one tool invocation to the session JSONL buffer."""
136
+ fields = _extract_fields(payload)
137
+ session_id = fields["session_id"]
138
+ tool_name = fields["tool_name"]
139
+ if not session_id or not tool_name:
140
+ return
141
+
142
+ tool_response = fields["tool_response"]
143
+ output_text = _flatten_tool_response_text(tool_response)
144
+ # ``_redact`` expects a dict; non-dict payloads (raw string, list, or None)
145
+ # are wrapped under ``_raw`` so the redaction path stays uniform without
146
+ # crashing on the unusual shape.
147
+ raw_tool_input = fields["tool_input"]
148
+ if isinstance(raw_tool_input, dict):
149
+ safe_tool_input: dict[str, Any] = _redact(raw_tool_input)
150
+ else:
151
+ safe_tool_input = {"_raw": _redact_string(str(raw_tool_input))}
152
+ record = {
153
+ "ts": int(time.time()),
154
+ "role": "Assistant_tool",
155
+ "tool_name": tool_name,
156
+ "tool_input": safe_tool_input,
157
+ "tool_output": _redact_string(output_text) if output_text else "",
158
+ "status": _derive_status(tool_response),
159
+ }
160
+ state.append(session_id, record)
@@ -0,0 +1,187 @@
1
+ """agent_end hook — finalize the assistant turn and publish to reflexio.
2
+
3
+ This handler is the openClaw analogue of claude-smart's ``stop.py``, but
4
+ substantially simpler. Per Phase 0 finding Q2, openClaw delivers the final
5
+ messages inline as ``event.messages: unknown[]``, so we read assistant text
6
+ directly from the payload instead of polling a transcript JSONL on disk.
7
+
8
+ Pipeline:
9
+ 1. Extract final assistant text from ``payload.messages``.
10
+ 2. Parse ``[oc:…]`` citation markers from that text and resolve them
11
+ against the per-session injected registry.
12
+ 3. Append an ``Assistant`` record (always, even when the text is empty —
13
+ ``state.unpublished_slice`` folds any buffered ``Assistant_tool``
14
+ records into the next ``Assistant`` turn's ``tools_used``).
15
+ 4. Drain the buffer to reflexio via ``publish.publish_unpublished``.
16
+
17
+ claude-smart's plan-mode decision scan and ``cs-cite`` Bash tool scan are
18
+ intentionally NOT ported — both rely on Claude-Code-specific transcript
19
+ shapes that openClaw does not emit.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ import time
26
+ from typing import Any
27
+
28
+ from openclaw_smart import ids, oc_cite, publish, state
29
+
30
+ _LOGGER = logging.getLogger(__name__)
31
+
32
+
33
+ def _extract_assistant_text(messages: Any) -> str:
34
+ """Return the final assistant turn's text from a messages list.
35
+
36
+ Walks ``messages`` from the end, collecting every contiguous assistant
37
+ entry up to the most recent non-assistant boundary, then joins their
38
+ text content. Mirrors claude-smart's transcript walker but works on
39
+ inline message dicts.
40
+
41
+ Args:
42
+ messages (Any): openClaw's ``event.messages`` blob, expected to be
43
+ a list of dicts with ``role`` and ``content`` fields.
44
+
45
+ Returns:
46
+ str: Concatenated assistant text, or ``""`` when none is found.
47
+ """
48
+ if not isinstance(messages, list):
49
+ return ""
50
+ collected: list[str] = []
51
+ for entry in reversed(messages):
52
+ if not isinstance(entry, dict):
53
+ continue
54
+ role = _entry_role(entry)
55
+ if role != "assistant":
56
+ # Boundary — stop collecting; everything before this belongs to
57
+ # earlier turns and has already been (or will be) published.
58
+ break
59
+ content = _entry_content(entry)
60
+ text_parts = _extract_text_blocks(content)
61
+ if text_parts:
62
+ # Prepend because we walked in reverse order.
63
+ collected = text_parts + collected
64
+ return "\n\n".join(part for part in collected if part)
65
+
66
+
67
+ def _entry_role(entry: dict[str, Any]) -> str:
68
+ """Read the role from an openClaw or Claude-Code-shaped message dict."""
69
+ role = entry.get("role")
70
+ if isinstance(role, str):
71
+ return role.lower()
72
+ message = entry.get("message")
73
+ if isinstance(message, dict):
74
+ nested = message.get("role")
75
+ if isinstance(nested, str):
76
+ return nested.lower()
77
+ return ""
78
+
79
+
80
+ def _entry_content(entry: dict[str, Any]) -> Any:
81
+ """Return the content payload from a message entry, accepting both shapes."""
82
+ if "content" in entry:
83
+ return entry["content"]
84
+ message = entry.get("message")
85
+ if isinstance(message, dict):
86
+ return message.get("content")
87
+ return None
88
+
89
+
90
+ def _extract_text_blocks(content: Any) -> list[str]:
91
+ """Return assistant-visible text from a message content payload.
92
+
93
+ Accepts:
94
+ - bare strings (``content: "hi"``)
95
+ - block lists (``content: [{type: "text", text: "hi"}, ...]``)
96
+ """
97
+ if isinstance(content, str):
98
+ return [content] if content else []
99
+ if not isinstance(content, list):
100
+ return []
101
+ out: list[str] = []
102
+ for block in content:
103
+ if not isinstance(block, dict):
104
+ continue
105
+ text = block.get("text")
106
+ if isinstance(text, str) and text:
107
+ out.append(text)
108
+ continue
109
+ inner = block.get("content")
110
+ if isinstance(inner, str) and inner:
111
+ out.append(inner)
112
+ return out
113
+
114
+
115
+ def _resolve_cited_items(session_id: str, cited_ids: list[str]) -> list[dict[str, Any]]:
116
+ """Map citation ids to ``{id, kind, title}`` entries via the session registry.
117
+
118
+ Unknown ids (model hallucinations, or items injected in a newer session
119
+ than this hook can see) are dropped. Duplicate ids within one turn
120
+ collapse to a single entry — the user-facing badge row doesn't need
121
+ the multiplicity.
122
+ """
123
+ if not cited_ids:
124
+ return []
125
+ registry = state.read_injected(session_id)
126
+ seen: set[str] = set()
127
+ resolved: list[dict[str, Any]] = []
128
+ for cid in cited_ids:
129
+ if cid in seen:
130
+ continue
131
+ entry = registry.get(cid)
132
+ if not entry:
133
+ continue
134
+ seen.add(cid)
135
+ item: dict[str, Any] = {
136
+ "id": entry.get("id", cid),
137
+ "kind": entry.get("kind", ""),
138
+ "title": entry.get("title", ""),
139
+ }
140
+ real_id = entry.get("real_id")
141
+ if real_id:
142
+ item["real_id"] = real_id
143
+ source_kind = entry.get("source_kind")
144
+ if isinstance(source_kind, str) and source_kind:
145
+ item["source_kind"] = source_kind
146
+ resolved.append(item)
147
+ return resolved
148
+
149
+
150
+ def handle(payload: dict[str, Any]) -> None:
151
+ """Finalize the current assistant turn and publish to reflexio.
152
+
153
+ Args:
154
+ payload (dict[str, Any]): openClaw event/ctx blob, expected to contain
155
+ ``sessionKey`` (or ``sessionId``), ``messages``, and optionally
156
+ ``agentId`` / ``workspaceDir``.
157
+ """
158
+ session_id = payload.get("sessionKey") or payload.get("sessionId")
159
+ if not session_id:
160
+ return
161
+
162
+ project_id = ids.resolve_project_id_with_fallback(
163
+ cwd=payload.get("workspaceDir"),
164
+ agent_id=payload.get("agentId"),
165
+ )
166
+
167
+ assistant_text = _extract_assistant_text(payload.get("messages"))
168
+ cited_ids = oc_cite.parse_text_citations(assistant_text)
169
+ cited_items = _resolve_cited_items(session_id, cited_ids)
170
+
171
+ now = int(time.time())
172
+ record: dict[str, Any] = {
173
+ "ts": now,
174
+ "role": "Assistant",
175
+ "content": assistant_text,
176
+ "user_id": project_id,
177
+ }
178
+ if cited_items:
179
+ record["cited_items"] = cited_items
180
+ state.append(session_id, record)
181
+
182
+ publish.publish_unpublished(
183
+ session_id=session_id,
184
+ project_id=project_id,
185
+ force_extraction=False,
186
+ skip_aggregation=False,
187
+ )