claude-smart 0.2.48 → 0.2.50

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 (193) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +11 -40
  3. package/bin/claude-smart.js +393 -55
  4. package/package.json +3 -2
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/.coverage +0 -0
  8. package/plugin/README.md +4 -3
  9. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
  10. package/plugin/dashboard/app/dashboard/page.tsx +6 -1
  11. package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
  12. package/plugin/dashboard/app/preferences/page.tsx +32 -35
  13. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
  14. package/plugin/dashboard/app/sessions/page.tsx +2 -0
  15. package/plugin/dashboard/app/skills/page.tsx +65 -50
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
  18. package/plugin/dashboard/components/common/host-badge.tsx +118 -0
  19. package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
  20. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  21. package/plugin/dashboard/components/common/page-header.tsx +3 -3
  22. package/plugin/dashboard/lib/config-file.ts +5 -1
  23. package/plugin/dashboard/lib/host-attribution.ts +62 -0
  24. package/plugin/dashboard/lib/session-reader.ts +40 -2
  25. package/plugin/dashboard/lib/types.ts +7 -1
  26. package/plugin/opencode/dist/server.mjs +60 -2
  27. package/plugin/opencode/server.mts +64 -2
  28. package/plugin/pyproject.toml +2 -2
  29. package/plugin/scripts/_lib.sh +255 -7
  30. package/plugin/scripts/backend-python-runner.py +46 -0
  31. package/plugin/scripts/backend-service.sh +766 -123
  32. package/plugin/scripts/codex-hook.js +79 -229
  33. package/plugin/scripts/dashboard-open.sh +6 -4
  34. package/plugin/scripts/dashboard-service.sh +118 -137
  35. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  36. package/plugin/scripts/hook_entry.sh +3 -0
  37. package/plugin/scripts/opencode-claude-compat.js +8 -1
  38. package/plugin/scripts/smart-install.sh +116 -21
  39. package/plugin/src/README.md +1 -1
  40. package/plugin/src/claude_smart/cli.py +93 -29
  41. package/plugin/src/claude_smart/context_inject.py +3 -0
  42. package/plugin/src/claude_smart/env_config.py +56 -11
  43. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  44. package/plugin/src/claude_smart/events/session_end.py +2 -1
  45. package/plugin/src/claude_smart/events/stop.py +3 -0
  46. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  47. package/plugin/src/claude_smart/internal_call.py +5 -2
  48. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  49. package/plugin/src/claude_smart/publish.py +59 -7
  50. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  51. package/plugin/src/claude_smart/runtime.py +15 -6
  52. package/plugin/src/claude_smart/state.py +211 -52
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +13 -0
  55. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  56. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  58. package/plugin/vendor/reflexio/reflexio/client/client.py +166 -9
  59. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  60. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  61. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  62. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +195 -25
  63. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  64. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  65. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  66. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  67. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  68. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  69. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  70. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  71. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  72. package/plugin/vendor/reflexio/reflexio/server/api.py +79 -5
  73. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  74. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  75. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  76. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  77. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +63 -6
  78. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  79. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
  80. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  81. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  82. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  83. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  84. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +199 -2
  85. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  86. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  90. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  91. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  92. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  93. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  94. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  95. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  96. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  97. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  98. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  99. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
  100. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  101. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  102. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  103. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  107. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  108. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  109. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  111. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  112. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  113. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  114. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  115. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +262 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  120. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  121. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  122. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  123. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  124. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  125. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  126. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
  127. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +640 -80
  129. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  131. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  133. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  135. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  136. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  137. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  138. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  139. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  140. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  141. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  142. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  143. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  144. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +166 -66
  145. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  146. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  150. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +190 -2
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +414 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +59 -7
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +57 -12
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +197 -12
  172. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +70 -11
  173. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
  174. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  175. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  176. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +269 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  178. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  179. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  180. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  181. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  182. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  183. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  184. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  185. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +82 -1
  186. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +74 -2
  187. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  188. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  189. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  190. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  191. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  192. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  193. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -85,6 +85,8 @@ write_failure() {
85
85
  local reason fp_hash
86
86
  reason="$1"
87
87
  fp_hash="$(claude_smart_install_fingerprint_hash "$PLUGIN_ROOT" "$HERE" 2>/dev/null || true)"
88
+ # bin/claude-smart.js reads this marker too: first line is the user-facing
89
+ # failure reason, and fingerprint=... marks which install attempt produced it.
88
90
  {
89
91
  printf '%s\n' "$reason"
90
92
  if [ -n "$fp_hash" ]; then
@@ -97,6 +99,22 @@ write_failure() {
97
99
  exit 0
98
100
  }
99
101
 
102
+ verify_windows_local_embedding_runtime() {
103
+ claude_smart_is_windows || return 0
104
+ [ "${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}" = "1" ] || return 0
105
+ if claude_smart_python_imports "$PLUGIN_ROOT" onnxruntime; then
106
+ return 0
107
+ fi
108
+ write_failure "Windows local embedding requires Microsoft Visual C++ Redistributable for onnxruntime; install the x64 redistributable from https://aka.ms/vs/17/release/vc_redist.x64.exe and rerun claude-smart install."
109
+ }
110
+
111
+ if [ "${1:-}" = "verify-windows-embedding" ]; then
112
+ # Node bootstrap runs this after uv sync so install-failed markers stay
113
+ # consistent with the shell installer path.
114
+ verify_windows_local_embedding_runtime
115
+ exit 0
116
+ fi
117
+
100
118
  install_fingerprint() {
101
119
  claude_smart_install_fingerprint "$PLUGIN_ROOT" "$HERE"
102
120
  }
@@ -112,6 +130,7 @@ install_complete() {
112
130
  grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.claude-smart/.env" || return 1
113
131
  grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.claude-smart/.env" || return 1
114
132
  grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_READ_ONLY=' "$HOME/.claude-smart/.env" || return 1
133
+ grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_HOST=' "$HOME/.claude-smart/.env" || return 1
115
134
  fi
116
135
  if [ -d "$PLUGIN_ROOT/dashboard" ]; then
117
136
  [ -d "$PLUGIN_ROOT/dashboard/.next" ] || [ -f "$MARKER_DIR/dashboard-build.pid" ] || [ -f "$(claude_smart_dashboard_unavailable_marker)" ] || return 1
@@ -130,7 +149,21 @@ install_vendored_reflexio() {
130
149
  local VENDORED_REFLEXIO PLUGIN_PYTHON
131
150
 
132
151
  VENDORED_REFLEXIO="$PLUGIN_ROOT/vendor/reflexio"
133
- [ -f "$VENDORED_REFLEXIO/pyproject.toml" ] || return 0
152
+ if [ ! -f "$VENDORED_REFLEXIO/pyproject.toml" ]; then
153
+ # A host plugin cache (~/.claude/plugins/cache/..., ~/.codex/plugins/cache/...)
154
+ # is only ever populated from the npm package, which always carries the
155
+ # vendored runtime. Missing it there means the plugin was installed straight
156
+ # from the GitHub marketplace, where the bundle is gitignored — the backend
157
+ # could never start. Fail here, at Setup, rather than at first session.
158
+ case "$PLUGIN_ROOT" in
159
+ */plugins/cache/*)
160
+ write_failure "bundled Reflexio runtime is missing from $VENDORED_REFLEXIO. This happens when claude-smart is installed from the GitHub marketplace, which does not carry the generated runtime. Install with: npx claude-smart install"
161
+ ;;
162
+ esac
163
+ # A source checkout legitimately has no vendor bundle (it is generated at
164
+ # pack time); reflexio comes from the plugin venv instead.
165
+ return 0
166
+ fi
134
167
 
135
168
  if claude_smart_is_windows; then
136
169
  PLUGIN_PYTHON="$PLUGIN_ROOT/.venv/Scripts/python.exe"
@@ -196,7 +229,7 @@ install_private_node() {
196
229
  local NODE_MIN_MAJOR NODE_MIN_MINOR NODE_LTS_MAJOR
197
230
  local node_os archive_ext reason node_arch node_platform base_url node_root
198
231
  local tmp_dir shasums_path archive_name ext_re install_dir archive_path
199
- local expected_hash actual_hash archive_win dest_win candidate_path next_link
232
+ local expected_hash actual_hash archive_win dest_win candidate_path next_link old_current
200
233
 
201
234
  NODE_MIN_MAJOR=20
202
235
  NODE_MIN_MINOR=9
@@ -361,19 +394,57 @@ install_private_node() {
361
394
  return 1
362
395
  fi
363
396
 
364
- next_link="$node_root/current.next.$$"
365
- if ln -s "$install_dir" "$next_link" 2>/dev/null; then
366
- if mv -Tf "$next_link" "$node_root/current" 2>/dev/null; then
367
- :
368
- elif mv -hf "$next_link" "$node_root/current" 2>/dev/null; then
369
- :
397
+ if claude_smart_is_windows; then
398
+ # Windows cannot rely on the POSIX symlink swap path; keep current recoverable.
399
+ old_current="$node_root/current.old.$$"
400
+ rm -rf "$old_current" 2>/dev/null || true
401
+ if [ -e "$node_root/current" ] || [ -L "$node_root/current" ]; then
402
+ if ! mv "$node_root/current" "$old_current"; then
403
+ rm -rf "$tmp_dir" "$install_dir"
404
+ reason="could not stage existing private Node.js install for replacement"
405
+ echo "[claude-smart] WARNING: $reason" >&2
406
+ claude_smart_write_dashboard_unavailable "$reason"
407
+ return 1
408
+ fi
409
+ fi
410
+ if mv "$install_dir" "$node_root/current"; then
411
+ rm -rf "$old_current" 2>/dev/null || true
370
412
  else
371
- rm -rf "$node_root/current"
372
- mv "$next_link" "$node_root/current"
413
+ reason="could not activate private Node.js install at $node_root/current"
414
+ if [ -e "$node_root/current" ] || [ -L "$node_root/current" ]; then
415
+ if ! rm -rf "$node_root/current"; then
416
+ reason="could not remove failed private Node.js install at $node_root/current; previous install remains at $old_current for manual recovery"
417
+ rm -rf "$tmp_dir" "$install_dir"
418
+ echo "[claude-smart] WARNING: $reason" >&2
419
+ claude_smart_write_dashboard_unavailable "$reason"
420
+ return 1
421
+ fi
422
+ fi
423
+ if [ -e "$old_current" ] || [ -L "$old_current" ]; then
424
+ if ! mv "$old_current" "$node_root/current"; then
425
+ reason="could not restore previous private Node.js install from $old_current after activation failure; previous install remains at $old_current for manual recovery"
426
+ fi
427
+ fi
428
+ rm -rf "$tmp_dir" "$install_dir"
429
+ echo "[claude-smart] WARNING: $reason" >&2
430
+ claude_smart_write_dashboard_unavailable "$reason"
431
+ return 1
373
432
  fi
374
433
  else
375
- rm -rf "$next_link" "$node_root/current"
376
- mv "$install_dir" "$node_root/current"
434
+ next_link="$node_root/current.next.$$"
435
+ if ln -s "$install_dir" "$next_link" 2>/dev/null; then
436
+ if mv -Tf "$next_link" "$node_root/current" 2>/dev/null; then
437
+ :
438
+ elif mv -hf "$next_link" "$node_root/current" 2>/dev/null; then
439
+ :
440
+ else
441
+ rm -rf "$node_root/current"
442
+ mv "$next_link" "$node_root/current"
443
+ fi
444
+ else
445
+ rm -rf "$next_link" "$node_root/current"
446
+ mv "$install_dir" "$node_root/current"
447
+ fi
377
448
  fi
378
449
 
379
450
  rm -rf "$tmp_dir"
@@ -399,6 +470,7 @@ fi
399
470
  preflight_supported_runtime_platform
400
471
 
401
472
  if install_complete; then
473
+ verify_windows_local_embedding_runtime
402
474
  start_backend_service
403
475
  claude_smart_emit_continue
404
476
  exit 0
@@ -489,7 +561,7 @@ fi
489
561
  # claude-smart's backend loads ~/.claude-smart/.env (backend-service.sh exports
490
562
  # REFLEXIO_ENV_FILE so reflexio's CLI reads it instead of ~/.reflexio/.env);
491
563
  # append our opt-in flags there so `reflexio services start` picks them up. Keep
492
- # this path in sync with env_config.REFLEXIO_ENV_PATH and backend-service.sh.
564
+ # this path in sync with env_config.CLAUDE_SMART_ENV_PATH and backend-service.sh.
493
565
  REFLEXIO_ENV="$HOME/.claude-smart/.env"
494
566
  mkdir -p "$(dirname "$REFLEXIO_ENV")"
495
567
  claude_smart_env_quote() {
@@ -562,6 +634,9 @@ claude_smart_prune_managed_env_keys_for_local() {
562
634
  }
563
635
 
564
636
  claude_smart_ensure_local_env_defaults() {
637
+ local local_cli_default local_embedding_default
638
+ local_cli_default="${CLAUDE_SMART_USE_LOCAL_CLI:-1}"
639
+ local_embedding_default="${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}"
565
640
  [ -z "${REFLEXIO_API_KEY:-}" ] || return 0
566
641
  mkdir -p "$(dirname "$REFLEXIO_ENV")"
567
642
  touch "$REFLEXIO_ENV"
@@ -569,23 +644,29 @@ claude_smart_ensure_local_env_defaults() {
569
644
  claude_smart_prune_managed_env_keys_for_local
570
645
  unset REFLEXIO_URL REFLEXIO_API_KEY REFLEXIO_USER_ID CLAUDE_SMART_MANAGED_SETUP
571
646
  if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
572
- printf '# Route reflexio generation through the local Claude Code CLI\n' >> "$REFLEXIO_ENV"
573
- claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_CLI "1"
574
- echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
647
+ printf '# Route reflexio generation through the configured local host CLI\n' >> "$REFLEXIO_ENV"
648
+ claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_CLI "$local_cli_default"
649
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=$local_cli_default to $REFLEXIO_ENV" >&2
575
650
  fi
576
651
  if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
577
652
  printf '# Use the in-process ONNX embedder (chromadb) - no API key for semantic search\n' >> "$REFLEXIO_ENV"
578
- claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_EMBEDDING "1"
579
- echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
653
+ claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_EMBEDDING "$local_embedding_default"
654
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=$local_embedding_default to $REFLEXIO_ENV" >&2
580
655
  fi
581
656
  if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_READ_ONLY=' "$REFLEXIO_ENV"; then
582
657
  claude_smart_env_upsert CLAUDE_SMART_READ_ONLY "0"
583
658
  echo "[claude-smart] appended CLAUDE_SMART_READ_ONLY=0 to $REFLEXIO_ENV" >&2
584
659
  fi
660
+ if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_HOST=' "$REFLEXIO_ENV"; then
661
+ claude_smart_env_upsert CLAUDE_SMART_HOST "claude-code"
662
+ echo "[claude-smart] appended CLAUDE_SMART_HOST=claude-code to $REFLEXIO_ENV" >&2
663
+ fi
585
664
  chmod 600 "$REFLEXIO_ENV"
586
665
  }
587
666
 
588
667
  claude_smart_ensure_local_env_defaults
668
+ # No-op outside Git Bash/MSYS/Cygwin; non-Windows installs must not depend on it.
669
+ verify_windows_local_embedding_runtime
589
670
 
590
671
  # Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
591
672
  # plugin backend port (8071). Matches the quoted and unquoted forms but
@@ -599,9 +680,23 @@ if [ -f "$REFLEXIO_ENV" ] && grep -qE '^REFLEXIO_URL=("http://localhost:8081/?"|
599
680
  echo "[claude-smart] migrated REFLEXIO_URL 8081 → 8071 in $REFLEXIO_ENV (backup at $REFLEXIO_ENV.bak)" >&2
600
681
  fi
601
682
 
602
- if ! command -v claude >/dev/null 2>&1; then
603
- echo "[claude-smart] WARNING: 'claude' CLI not on PATH — reflexio extractors will have no LLM until it's installed" >&2
604
- fi
683
+ case "${CLAUDE_SMART_HOST:-claude-code}" in
684
+ opencode)
685
+ if [ -z "${CLAUDE_SMART_OPENCODE_PATH:-}" ] && ! command -v opencode >/dev/null 2>&1; then
686
+ echo "[claude-smart] WARNING: 'opencode' CLI not on PATH and CLAUDE_SMART_OPENCODE_PATH is not set — reflexio extractors will have no LLM until OpenCode is installed" >&2
687
+ fi
688
+ ;;
689
+ codex)
690
+ if ! command -v codex >/dev/null 2>&1; then
691
+ echo "[claude-smart] WARNING: 'codex' CLI not on PATH — reflexio extractors will have no LLM until Codex is installed" >&2
692
+ fi
693
+ ;;
694
+ *)
695
+ if ! command -v claude >/dev/null 2>&1; then
696
+ echo "[claude-smart] WARNING: 'claude' CLI not on PATH — reflexio extractors will have no LLM until Claude Code is installed" >&2
697
+ fi
698
+ ;;
699
+ esac
605
700
 
606
701
  LEGACY_CS_CITE="$HOME/.claude-smart/bin/cs-cite"
607
702
  if [ -e "$LEGACY_CS_CITE" ]; then
@@ -28,7 +28,7 @@ Description: Python package powering the claude-smart plugin — hook handlers t
28
28
 
29
29
  | File | Purpose |
30
30
  |------|---------|
31
- | `env_config.py` | Parses `~/.claude-smart/.env` (`REFLEXIO_URL`, `REFLEXIO_API_KEY`, `CLAUDE_SMART_READ_ONLY`, local embedding/CLI flags). |
31
+ | `env_config.py` | Parses `~/.claude-smart/.env` (`REFLEXIO_URL`, `REFLEXIO_API_KEY`, `CLAUDE_SMART_READ_ONLY`, `CLAUDE_SMART_HOST`, local embedding/CLI flags). |
32
32
  | `runtime.py` | Host detection (Claude Code vs Codex); shared agent version. |
33
33
  | `ids.py` | Session / project ID generation and resolution. |
34
34
  | `context_inject.py`, `context_format.py`, `query_compose.py`, `cs_cite.py` | Build search queries, format learned skills as markdown, inject into context, format citations. |
@@ -37,10 +37,14 @@ from pathlib import Path
37
37
  from urllib.parse import urlparse
38
38
  from urllib.request import url2pathname
39
39
 
40
- from claude_smart import context_format, cs_cite, env_config, ids, publish, state
40
+ from claude_smart import context_format, cs_cite, env_config, ids, publish, runtime, state
41
41
  from claude_smart.reflexio_adapter import Adapter
42
42
 
43
- _REFLEXIO_ENV_PATH = env_config.REFLEXIO_ENV_PATH
43
+ _HOST_CLAUDE_CODE = runtime.HOST_CLAUDE_CODE
44
+ _HOST_CODEX = runtime.HOST_CODEX
45
+ _HOST_OPENCODE = runtime.HOST_OPENCODE
46
+ _HOST_CHOICES = (_HOST_CLAUDE_CODE, _HOST_CODEX, _HOST_OPENCODE)
47
+ _CLAUDE_SMART_ENV_PATH = env_config.CLAUDE_SMART_ENV_PATH
44
48
  _MANAGED_REFLEXIO_URL = env_config.MANAGED_REFLEXIO_URL
45
49
  _PLUGIN_SPEC = "claude-smart@reflexioai"
46
50
  _CODEX_MARKETPLACE_NAME = "reflexioai"
@@ -99,6 +103,7 @@ _DEFAULT_STORAGE_ROOT = _REFLEXIO_DIR / "data"
99
103
  _REFLEXIO_ORG_ID = "claude-smart"
100
104
  _REFLEXIO_CONFIG_PATH = _REFLEXIO_DIR / "configs" / f"config_{_REFLEXIO_ORG_ID}.json"
101
105
  _LOCAL_STORAGE_ENV = "LOCAL_STORAGE_PATH"
106
+ _OPENCODE_PATH_ENV = "CLAUDE_SMART_OPENCODE_PATH"
102
107
  _CODEX_REQUIRED_FILES = (
103
108
  Path(".agents/plugins/marketplace.json"),
104
109
  Path("plugin/.codex-plugin/plugin.json"),
@@ -927,15 +932,20 @@ def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
927
932
  return False, output or "Codex CLI does not expose plugin marketplace commands"
928
933
 
929
934
 
930
- def _configure_reflexio_setup() -> bool:
935
+ def _configure_reflexio_setup(host: str = _HOST_CLAUDE_CODE) -> bool:
931
936
  """Load setup state and ensure local defaults when unmanaged.
932
937
 
938
+ Args:
939
+ host: Host integration being installed. Persisted in local mode so
940
+ later backend restarts can wire the same extraction bridge even
941
+ when the host plugin server is not the process launcher.
942
+
933
943
  Returns:
934
944
  bool: Whether read-only mode is enabled.
935
945
  """
936
- env_config.load_reflexio_env(_REFLEXIO_ENV_PATH)
946
+ env_config.load_reflexio_env(_CLAUDE_SMART_ENV_PATH)
937
947
  try:
938
- env_text = _REFLEXIO_ENV_PATH.read_text()
948
+ env_text = _CLAUDE_SMART_ENV_PATH.read_text()
939
949
  except OSError:
940
950
  env_text = ""
941
951
  read_only_value = ""
@@ -975,9 +985,9 @@ def _configure_reflexio_setup() -> bool:
975
985
  os.environ.pop(env_config.REFLEXIO_API_KEY_ENV, None)
976
986
  os.environ.pop("REFLEXIO_USER_ID", None)
977
987
  os.environ.pop("CLAUDE_SMART_MANAGED_SETUP", None)
978
- added = env_config.ensure_local_env_defaults(_REFLEXIO_ENV_PATH)
988
+ added = env_config.ensure_local_env_defaults(_CLAUDE_SMART_ENV_PATH, host=host)
979
989
  if added:
980
- sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
990
+ sys.stdout.write(f"Seeded {_CLAUDE_SMART_ENV_PATH} with {', '.join(added)}.\n")
981
991
  return read_only
982
992
 
983
993
 
@@ -1174,7 +1184,7 @@ def _patch_opencode_plugin_config(
1174
1184
 
1175
1185
 
1176
1186
  def _has_extraction_provider() -> bool:
1177
- env_config.load_reflexio_env(_REFLEXIO_ENV_PATH)
1187
+ env_config.load_reflexio_env(_CLAUDE_SMART_ENV_PATH)
1178
1188
  if os.environ.get(env_config.REFLEXIO_API_KEY_ENV, "").strip():
1179
1189
  return True
1180
1190
  cli_path = os.environ.get("CLAUDE_SMART_CLI_PATH", "").strip()
@@ -1182,7 +1192,7 @@ def _has_extraction_provider() -> bool:
1182
1192
  resolved = Path(cli_path).expanduser()
1183
1193
  if resolved.is_file() and os.access(resolved, os.X_OK):
1184
1194
  return True
1185
- return bool(shutil.which("claude") or shutil.which("codex") or shutil.which("opencode"))
1195
+ return bool(shutil.which("claude") or shutil.which("codex") or _resolve_opencode_path())
1186
1196
 
1187
1197
 
1188
1198
  def _extraction_provider_error() -> str:
@@ -1193,6 +1203,41 @@ def _extraction_provider_error() -> str:
1193
1203
  )
1194
1204
 
1195
1205
 
1206
+ def _has_opencode_cli() -> bool:
1207
+ return _resolve_opencode_path() is not None
1208
+
1209
+
1210
+ def _resolve_opencode_path() -> str | None:
1211
+ opencode_path = os.environ.get(_OPENCODE_PATH_ENV, "").strip()
1212
+ if opencode_path:
1213
+ resolved = Path(opencode_path).expanduser()
1214
+ if resolved.is_file() and os.access(resolved, os.X_OK):
1215
+ return str(resolved)
1216
+ return shutil.which("opencode")
1217
+
1218
+
1219
+ def _persist_opencode_path() -> list[str]:
1220
+ resolved = _resolve_opencode_path()
1221
+ if not resolved:
1222
+ return []
1223
+ os.environ[_OPENCODE_PATH_ENV] = resolved
1224
+ return env_config.set_env_vars(_CLAUDE_SMART_ENV_PATH, {_OPENCODE_PATH_ENV: resolved})
1225
+
1226
+
1227
+ def _opencode_prerequisite_error() -> str | None:
1228
+ if not _has_opencode_cli():
1229
+ return (
1230
+ "error: OpenCode CLI not found on PATH. Install OpenCode first, "
1231
+ "or set CLAUDE_SMART_OPENCODE_PATH to the OpenCode executable.\n"
1232
+ )
1233
+ if os.name == "nt" and not _resolve_bash():
1234
+ return (
1235
+ "error: Git Bash is required for claude-smart OpenCode support on Windows. "
1236
+ "Install Git for Windows and ensure bash.exe is on PATH, or run OpenCode from WSL.\n"
1237
+ )
1238
+ return None
1239
+
1240
+
1196
1241
  def _opencode_install_supported_from_this_package() -> bool:
1197
1242
  return (_SCRIPTS_DIR / "smart-install.sh").is_file()
1198
1243
 
@@ -1332,7 +1377,12 @@ def cmd_install_opencode(args: argparse.Namespace) -> int:
1332
1377
  "Run `npx claude-smart install --host opencode`.\n"
1333
1378
  )
1334
1379
  return 1
1335
- read_only = _configure_reflexio_setup()
1380
+ prerequisite_error = _opencode_prerequisite_error()
1381
+ if prerequisite_error:
1382
+ sys.stderr.write(prerequisite_error)
1383
+ return 1
1384
+ read_only = _configure_reflexio_setup(host=_HOST_OPENCODE)
1385
+ _persist_opencode_path()
1336
1386
  if not _has_extraction_provider():
1337
1387
  sys.stderr.write(_extraction_provider_error())
1338
1388
  return 1
@@ -1387,7 +1437,7 @@ def cmd_install_codex(args: argparse.Namespace) -> int:
1387
1437
  "error: 'uv' not found on PATH. Install uv or restart your shell.\n"
1388
1438
  )
1389
1439
  return 1
1390
- read_only = _configure_reflexio_setup()
1440
+ read_only = _configure_reflexio_setup(host=_HOST_CODEX)
1391
1441
 
1392
1442
  missing = _missing_codex_marketplace_files(_REPO_ROOT)
1393
1443
  if missing:
@@ -1485,9 +1535,9 @@ def cmd_install(args: argparse.Namespace) -> int:
1485
1535
  Returns:
1486
1536
  int: 0 on success, non-zero if the ``claude`` CLI is missing or fails.
1487
1537
  """
1488
- if getattr(args, "host", "claude-code") == "codex":
1538
+ if getattr(args, "host", _HOST_CLAUDE_CODE) == _HOST_CODEX:
1489
1539
  return cmd_install_codex(args)
1490
- if getattr(args, "host", "claude-code") == "opencode":
1540
+ if getattr(args, "host", _HOST_CLAUDE_CODE) == _HOST_OPENCODE:
1491
1541
  return cmd_install_opencode(args)
1492
1542
 
1493
1543
  if not shutil.which("claude"):
@@ -1496,7 +1546,7 @@ def cmd_install(args: argparse.Namespace) -> int:
1496
1546
  "Install Claude Code first: https://claude.com/claude-code\n"
1497
1547
  )
1498
1548
  return 1
1499
- read_only = _configure_reflexio_setup()
1549
+ read_only = _configure_reflexio_setup(host=_HOST_CLAUDE_CODE)
1500
1550
 
1501
1551
  refresh_existing = bool(getattr(args, "refresh_existing", False))
1502
1552
  for cmd in (
@@ -1559,16 +1609,16 @@ def cmd_update(args: argparse.Namespace) -> int:
1559
1609
  Returns:
1560
1610
  int: 0 on success, non-zero if the host CLI is missing or install fails.
1561
1611
  """
1562
- if getattr(args, "host", "claude-code") == "codex":
1612
+ if getattr(args, "host", _HOST_CLAUDE_CODE) == _HOST_CODEX:
1563
1613
  return cmd_update_codex(args)
1564
- if getattr(args, "host", "claude-code") == "opencode":
1614
+ if getattr(args, "host", _HOST_CLAUDE_CODE) == _HOST_OPENCODE:
1565
1615
  return cmd_update_opencode(args)
1566
1616
 
1567
1617
  _run_service(_DASHBOARD_SCRIPT, "stop")
1568
1618
  _run_service(_BACKEND_SCRIPT, "stop")
1569
1619
  sys.stdout.write("Updating claude-smart by reinstalling from this package...\n")
1570
1620
  install_args = argparse.Namespace(**vars(args), refresh_existing=True)
1571
- install_args.host = "claude-code"
1621
+ install_args.host = _HOST_CLAUDE_CODE
1572
1622
  return cmd_install(install_args)
1573
1623
 
1574
1624
 
@@ -1585,7 +1635,7 @@ def cmd_update_codex(_args: argparse.Namespace) -> int:
1585
1635
  "Updating claude-smart Codex support by reinstalling from this package...\n"
1586
1636
  )
1587
1637
  install_args = argparse.Namespace(**vars(_args))
1588
- install_args.host = "codex"
1638
+ install_args.host = _HOST_CODEX
1589
1639
  return cmd_install_codex(install_args)
1590
1640
 
1591
1641
 
@@ -1596,7 +1646,7 @@ def cmd_update_opencode(args: argparse.Namespace) -> int:
1596
1646
  "Updating claude-smart OpenCode support by reinstalling from this package...\n"
1597
1647
  )
1598
1648
  install_args = argparse.Namespace(**vars(args))
1599
- install_args.host = "opencode"
1649
+ install_args.host = _HOST_OPENCODE
1600
1650
  return cmd_install_opencode(install_args)
1601
1651
 
1602
1652
 
@@ -1612,9 +1662,9 @@ def cmd_uninstall(_args: argparse.Namespace) -> int:
1612
1662
  Returns:
1613
1663
  int: 0 on success, non-zero if the ``claude`` CLI is missing or fails.
1614
1664
  """
1615
- if getattr(_args, "host", "claude-code") == "codex":
1665
+ if getattr(_args, "host", _HOST_CLAUDE_CODE) == _HOST_CODEX:
1616
1666
  return cmd_uninstall_codex(_args)
1617
- if getattr(_args, "host", "claude-code") == "opencode":
1667
+ if getattr(_args, "host", _HOST_CLAUDE_CODE) == _HOST_OPENCODE:
1618
1668
  return cmd_uninstall_opencode(_args)
1619
1669
 
1620
1670
  if not shutil.which("claude"):
@@ -1761,7 +1811,7 @@ def _reflexio_show_footer() -> str:
1761
1811
  str: A leading-separator markdown footer ending in a newline.
1762
1812
  """
1763
1813
  return (
1764
- f"\n---\n⭐ claude-smart is powered by [reflexio]({cs_cite.REFLEXIO_REPO_URL})"
1814
+ f"\n---\nclaude-smart is powered by [reflexio]({cs_cite.REFLEXIO_REPO_URL})"
1765
1815
  f" — if it helps you, [star it on GitHub]({cs_cite.REFLEXIO_REPO_URL}).\n"
1766
1816
  )
1767
1817
 
@@ -1949,7 +1999,7 @@ def _resolve_absolute_path(raw_path: str | Path, *, source: str) -> Path:
1949
1999
  def _effective_storage_root() -> Path:
1950
2000
  raw = os.environ.get(_LOCAL_STORAGE_ENV, "").strip()
1951
2001
  if not raw:
1952
- raw = _read_dotenv_value(_REFLEXIO_ENV_PATH, _LOCAL_STORAGE_ENV) or ""
2002
+ raw = _read_dotenv_value(_CLAUDE_SMART_ENV_PATH, _LOCAL_STORAGE_ENV) or ""
1953
2003
  return _resolve_absolute_path(
1954
2004
  raw or _DEFAULT_STORAGE_ROOT, source=_LOCAL_STORAGE_ENV
1955
2005
  )
@@ -2135,6 +2185,20 @@ def cmd_restart(args: argparse.Namespace) -> int:
2135
2185
  return 0
2136
2186
 
2137
2187
  if do_backend:
2188
+ # Preparing the replacement can reinstall the vendored Reflexio if its
2189
+ # version drifted, so this both validates and repairs. It must succeed
2190
+ # before we stop anything: a backend we cannot replace is better left
2191
+ # running.
2192
+ sys.stdout.write("Preparing reflexio backend replacement…\n")
2193
+ preflight_rc = _run_service(_BACKEND_SCRIPT, "preflight")
2194
+ if preflight_rc != 0:
2195
+ sys.stderr.write(
2196
+ "error: reflexio backend replacement failed preflight; "
2197
+ "existing services were not changed.\n"
2198
+ "Repair the bundled runtime with: npx claude-smart update\n"
2199
+ )
2200
+ return preflight_rc
2201
+
2138
2202
  sys.stdout.write("Stopping reflexio backend…\n")
2139
2203
  _run_service(_BACKEND_SCRIPT, "stop")
2140
2204
  if do_dashboard:
@@ -2449,8 +2513,8 @@ def _build_parser() -> argparse.ArgumentParser:
2449
2513
  inst = sub.add_parser("install", help="Install claude-smart")
2450
2514
  inst.add_argument(
2451
2515
  "--host",
2452
- choices=("claude-code", "codex", "opencode"),
2453
- default="claude-code",
2516
+ choices=_HOST_CHOICES,
2517
+ default=_HOST_CLAUDE_CODE,
2454
2518
  help="Install target host",
2455
2519
  )
2456
2520
  inst.add_argument(
@@ -2464,8 +2528,8 @@ def _build_parser() -> argparse.ArgumentParser:
2464
2528
  upd = sub.add_parser("update", help="Update claude-smart to the latest version")
2465
2529
  upd.add_argument(
2466
2530
  "--host",
2467
- choices=("claude-code", "codex", "opencode"),
2468
- default="claude-code",
2531
+ choices=_HOST_CHOICES,
2532
+ default=_HOST_CLAUDE_CODE,
2469
2533
  help="Update target host",
2470
2534
  )
2471
2535
  upd.add_argument(
@@ -2479,8 +2543,8 @@ def _build_parser() -> argparse.ArgumentParser:
2479
2543
  uni = sub.add_parser("uninstall", help="Remove claude-smart")
2480
2544
  uni.add_argument(
2481
2545
  "--host",
2482
- choices=("claude-code", "codex", "opencode"),
2483
- default="claude-code",
2546
+ choices=_HOST_CHOICES,
2547
+ default=_HOST_CLAUDE_CODE,
2484
2548
  help="Uninstall target host",
2485
2549
  )
2486
2550
  uni.add_argument(
@@ -59,6 +59,9 @@ def emit_context(
59
59
  project_id=project_id,
60
60
  query=query,
61
61
  top_k=top_k,
62
+ # Scopes server-side dedup: rules already injected into this session
63
+ # are not returned again; next-best matches backfill instead.
64
+ session_id=session_id or None,
62
65
  )
63
66
  renderer = (
64
67
  context_format.render_inline_compact_with_registry
@@ -13,17 +13,26 @@ from __future__ import annotations
13
13
  import os
14
14
  from pathlib import Path
15
15
 
16
- REFLEXIO_ENV_PATH = Path.home() / ".claude-smart" / ".env"
16
+ from claude_smart import runtime
17
+
18
+ CLAUDE_SMART_ENV_PATH = Path.home() / ".claude-smart" / ".env"
19
+ # Deprecated internal alias kept so older imports do not fail abruptly.
20
+ REFLEXIO_ENV_PATH = CLAUDE_SMART_ENV_PATH
17
21
  MANAGED_REFLEXIO_URL = "https://www.reflexio.ai/"
18
22
  REFLEXIO_URL_ENV = "REFLEXIO_URL"
19
23
  REFLEXIO_API_KEY_ENV = "REFLEXIO_API_KEY"
20
24
  CLAUDE_SMART_READ_ONLY_ENV = "CLAUDE_SMART_READ_ONLY"
21
25
  CLAUDE_SMART_USE_LOCAL_CLI_ENV = "CLAUDE_SMART_USE_LOCAL_CLI"
22
26
  CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV = "CLAUDE_SMART_USE_LOCAL_EMBEDDING"
27
+ CLAUDE_SMART_HOST_ENV = "CLAUDE_SMART_HOST"
28
+ DEFAULT_CLAUDE_SMART_HOST = runtime.HOST_CLAUDE_CODE
23
29
 
24
30
  _LOCAL_DEFAULT_ENTRIES = (
25
31
  (
26
- "# Route reflexio generation through the local Claude Code CLI",
32
+ "# Set to 1 to route reflexio generation through the active host CLI "
33
+ "(claude for Claude Code, the host compatibility bridge for OpenCode/Codex). "
34
+ "Set to 0 to disable that provider so reflexio auto-detects a cloud LLM "
35
+ "key (anthropic, minimax, openai, etc.) from ~/.claude-smart/.env instead.",
27
36
  CLAUDE_SMART_USE_LOCAL_CLI_ENV,
28
37
  "1",
29
38
  ),
@@ -33,7 +42,12 @@ _LOCAL_DEFAULT_ENTRIES = (
33
42
  "1",
34
43
  ),
35
44
  (None, CLAUDE_SMART_READ_ONLY_ENV, "0"),
45
+ (None, CLAUDE_SMART_HOST_ENV, DEFAULT_CLAUDE_SMART_HOST),
36
46
  )
47
+ _ENV_OVERRIDABLE_LOCAL_DEFAULT_KEYS = {
48
+ CLAUDE_SMART_USE_LOCAL_CLI_ENV,
49
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV,
50
+ }
37
51
  _LOCAL_MODE_PRUNE_KEYS = {
38
52
  REFLEXIO_URL_ENV,
39
53
  REFLEXIO_API_KEY_ENV,
@@ -69,7 +83,7 @@ def parse_env_line(line: str) -> tuple[str, str] | None:
69
83
 
70
84
  def load_reflexio_env(path: Path | None = None) -> None:
71
85
  """Load ``~/.claude-smart/.env`` into ``os.environ`` without overriding values."""
72
- path = path or REFLEXIO_ENV_PATH
86
+ path = path or CLAUDE_SMART_ENV_PATH
73
87
  try:
74
88
  text = path.read_text()
75
89
  except OSError:
@@ -118,14 +132,19 @@ def set_env_vars(path: Path, values: dict[str, str]) -> list[str]:
118
132
  return added
119
133
 
120
134
 
121
- def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
135
+ def ensure_local_env_defaults(
136
+ path: Path | None = None,
137
+ host: str = DEFAULT_CLAUDE_SMART_HOST,
138
+ ) -> list[str]:
122
139
  """Create or augment ``~/.claude-smart/.env`` for claude-smart local mode.
123
140
 
124
- Existing active assignments win. This repairs first installs and deleted
125
- env files without clobbering explicit user overrides such as
126
- ``CLAUDE_SMART_READ_ONLY=1``.
141
+ Existing active assignments win except ``CLAUDE_SMART_HOST``, which is
142
+ install-selected state and must follow the host currently being installed.
143
+ This repairs first installs and deleted env files without clobbering
144
+ explicit user overrides such as ``CLAUDE_SMART_READ_ONLY=1``.
127
145
  """
128
- path = path or REFLEXIO_ENV_PATH
146
+ install_host = host
147
+ path = path or CLAUDE_SMART_ENV_PATH
129
148
  path.parent.mkdir(parents=True, exist_ok=True)
130
149
  try:
131
150
  existing = path.read_text()
@@ -135,6 +154,8 @@ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
135
154
  present: set[str] = set()
136
155
  kept_lines: list[str] = []
137
156
  pruned = False
157
+ changed = False
158
+ host_written = False
138
159
  for line in existing.splitlines():
139
160
  parsed = parse_env_line(line)
140
161
  if parsed is not None:
@@ -142,6 +163,19 @@ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
142
163
  if key in _LOCAL_MODE_PRUNE_KEYS:
143
164
  pruned = True
144
165
  continue
166
+ if key == CLAUDE_SMART_HOST_ENV:
167
+ present.add(key)
168
+ if not host_written:
169
+ replacement = (
170
+ f"{CLAUDE_SMART_HOST_ENV}={_escape_env_value(install_host)}"
171
+ )
172
+ kept_lines.append(replacement)
173
+ host_written = True
174
+ if line != replacement or _value != install_host:
175
+ changed = True
176
+ else:
177
+ changed = True
178
+ continue
145
179
  present.add(key)
146
180
  kept_lines.append(line)
147
181
 
@@ -150,15 +184,16 @@ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
150
184
  for comment, key, value in _LOCAL_DEFAULT_ENTRIES:
151
185
  if key in present:
152
186
  continue
187
+ effective_value = _resolve_local_env_default(key, value, install_host)
153
188
  if comment:
154
189
  additions.append(comment)
155
190
  if key == CLAUDE_SMART_READ_ONLY_ENV:
156
- additions.append(f'{key}="{_escape_env_value(value)}"')
191
+ additions.append(f'{key}="{_escape_env_value(effective_value)}"')
157
192
  else:
158
- additions.append(f"{key}={_escape_env_value(value)}")
193
+ additions.append(f"{key}={_escape_env_value(effective_value)}")
159
194
  added_keys.append(key)
160
195
 
161
- if additions or pruned:
196
+ if additions or pruned or changed:
162
197
  content = "\n".join(kept_lines)
163
198
  if additions:
164
199
  prefix = "" if not content or content.endswith("\n") else "\n"
@@ -171,6 +206,16 @@ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
171
206
  return added_keys
172
207
 
173
208
 
209
+ def _resolve_local_env_default(key: str, fallback: str, install_host: str) -> str:
210
+ if key == CLAUDE_SMART_HOST_ENV:
211
+ return install_host
212
+ if key in _ENV_OVERRIDABLE_LOCAL_DEFAULT_KEYS:
213
+ explicit = os.environ.get(key, "").strip()
214
+ if explicit:
215
+ return explicit
216
+ return fallback
217
+
218
+
174
219
  def env_truthy(name: str) -> bool:
175
220
  """Return True when an environment flag is explicitly enabled."""
176
221
  return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}