claude-smart 0.2.49 → 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 (177) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +10 -43
  3. package/bin/claude-smart.js +105 -0
  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/pyproject.toml +1 -1
  27. package/plugin/scripts/_lib.sh +197 -1
  28. package/plugin/scripts/backend-python-runner.py +46 -0
  29. package/plugin/scripts/backend-service.sh +757 -119
  30. package/plugin/scripts/codex-hook.js +63 -225
  31. package/plugin/scripts/dashboard-open.sh +6 -4
  32. package/plugin/scripts/dashboard-service.sh +117 -136
  33. package/plugin/scripts/hook_entry.sh +3 -0
  34. package/plugin/scripts/smart-install.sh +15 -1
  35. package/plugin/src/claude_smart/cli.py +14 -0
  36. package/plugin/src/claude_smart/context_inject.py +3 -0
  37. package/plugin/src/claude_smart/env_config.py +4 -1
  38. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  39. package/plugin/src/claude_smart/events/session_end.py +2 -1
  40. package/plugin/src/claude_smart/events/stop.py +3 -0
  41. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  42. package/plugin/src/claude_smart/internal_call.py +5 -2
  43. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  44. package/plugin/src/claude_smart/publish.py +59 -7
  45. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  46. package/plugin/src/claude_smart/runtime.py +15 -6
  47. package/plugin/src/claude_smart/state.py +211 -52
  48. package/plugin/uv.lock +1 -1
  49. package/plugin/vendor/reflexio/.env.example +13 -0
  50. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  51. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  52. package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
  53. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  54. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  55. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  56. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
  57. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  58. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  59. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  60. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  61. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  62. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  63. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  64. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  65. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
  67. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  68. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  69. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  70. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  71. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
  72. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  73. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  74. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  75. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  76. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  77. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
  78. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  79. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  80. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  81. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  82. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  83. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  84. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  85. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  86. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  90. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  91. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  92. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  93. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  94. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  95. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  96. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  98. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  99. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  100. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  101. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  103. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  104. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  105. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  106. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
  109. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  110. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  111. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  112. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  113. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  114. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  115. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  116. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
  118. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  120. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  122. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  124. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  125. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  126. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  127. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  128. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  129. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  130. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  131. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  132. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  133. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
  134. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  135. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  139. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  172. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  173. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  174. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  175. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  176. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env bash
2
- # Auto-start the claude-smart Next.js dashboard (port 3001) if it's not
3
- # already running. Mirrors how claude-mem boots its worker on SessionStart:
2
+ # Auto-start the claude-smart Next.js dashboard if it's not already running.
3
+ # Mirrors how claude-mem boots its worker on SessionStart:
4
4
  # detached, returns immediately so the hook doesn't block the session.
5
5
  #
6
6
  # Subcommands:
7
- # start probe the port; spawn `npm run start` if our dashboard
8
- # isn't already answering. Never builds in foreground — if
7
+ # start probe the port; spawn local `next start` if no claude-smart
8
+ # dashboard is already answering. Never builds in foreground — if
9
9
  # .next is missing, logs and bails (Setup is responsible for
10
10
  # the build; rerun it or restart Claude Code to retry).
11
11
  # stop kill the recorded process group, and (if our dashboard
@@ -28,7 +28,10 @@ fi
28
28
  claude_smart_prepend_node_bins
29
29
  claude_smart_source_reflexio_env
30
30
 
31
- PORT=3001
31
+ BACKEND_PORT="${BACKEND_PORT:-8071}"
32
+ PORT="${DASHBOARD_PORT:-${PORT:-3001}}"
33
+ export BACKEND_PORT
34
+ claude_smart_derive_reflexio_url_from_backend_port
32
35
 
33
36
  PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
34
37
  claude_smart_reexec_stable_plugin_root_if_needed "$PLUGIN_ROOT" "dashboard-service.sh" "$@"
@@ -48,116 +51,46 @@ kill_group() {
48
51
  claude_smart_kill_tree "$1"
49
52
  }
50
53
 
54
+ dashboard_pid_command_matches() {
55
+ pid="$1"
56
+ cmdline="$(claude_smart_pid_command "$pid" | tr '\n' ' ' || true)"
57
+ [ -n "$cmdline" ] || return 1
58
+ case "$cmdline" in
59
+ *"$DASHBOARD_DIR"*|*next-server*|*next*start*|*node*next*|*claude-smart*dashboard*) return 0 ;;
60
+ *) return 1 ;;
61
+ esac
62
+ }
63
+
64
+ kill_recorded_dashboard() {
65
+ if [ -f "$PID_FILE" ]; then
66
+ pid="$(cat "$PID_FILE" 2>/dev/null || true)"
67
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && dashboard_pid_command_matches "$pid"; then
68
+ kill_group "$pid"
69
+ fi
70
+ rm -f "$PID_FILE"
71
+ fi
72
+ }
73
+
51
74
  # True if the marker header served by app/api/health is present on the
52
75
  # port. Requires curl — absence is reported as false. This deliberately
53
76
  # accepts any claude-smart dashboard, including stale cache versions, so
54
77
  # stop/uninstall can safely reap them without touching foreign listeners.
55
78
  marker_responds() {
56
79
  command -v curl >/dev/null 2>&1 || return 1
57
- curl -sfI "http://127.0.0.1:$PORT/api/health" 2>/dev/null \
80
+ curl -sfI --connect-timeout 1 --max-time 2 "http://127.0.0.1:$PORT/api/health" 2>/dev/null \
58
81
  | grep -qi '^x-claude-smart-dashboard:'
59
82
  }
60
83
 
61
- dashboard_health_headers() {
62
- command -v curl >/dev/null 2>&1 || return 1
63
- curl -sfI --connect-timeout 2 --max-time 5 "http://127.0.0.1:$PORT/api/health" 2>/dev/null
64
- }
65
-
66
- header_value_from() {
67
- header_name="$1"
68
- awk -v wanted="$header_name" '
69
- BEGIN { wanted = tolower(wanted) ":" }
70
- {
71
- line = $0
72
- sub(/\r$/, "", line)
73
- lower = tolower(line)
74
- if (index(lower, wanted) == 1) {
75
- sub(/^[^:]*:[[:space:]]*/, "", line)
76
- print line
77
- exit
78
- }
79
- }
80
- '
81
- }
82
-
83
- canonical_dir() {
84
- dir="$1"
85
- (cd "$dir" 2>/dev/null && pwd -P) || printf '%s\n' "$dir"
86
- }
87
-
88
- normalize_identity_path() {
89
- path="$1"
90
- if claude_smart_is_windows; then
91
- if command -v cygpath >/dev/null 2>&1; then
92
- path="$(cygpath -u "$path" 2>/dev/null || printf '%s\n' "$path")"
93
- else
94
- path="$(
95
- printf '%s\n' "$path" | awk '{
96
- gsub(/\\/, "/")
97
- if ($0 ~ /^[A-Za-z]:/) {
98
- drive = tolower(substr($0, 1, 1))
99
- sub(/^[A-Za-z]:/, "/" drive)
100
- }
101
- print
102
- }'
103
- )"
104
- fi
105
- fi
106
- while [ "${path%/}" != "$path" ] && [ "$path" != "/" ]; do
107
- path="${path%/}"
108
- done
109
- printf '%s\n' "$path"
110
- }
111
-
112
- # True only if *this plugin root's* dashboard is on the port. The generic
113
- # x-claude-smart-dashboard marker is intentionally not enough: after an
114
- # install/update, an older cache can keep serving port 3001 until restarted.
115
- dashboard_matches_current_root() {
116
- expected_root="$(normalize_identity_path "$(canonical_dir "$PLUGIN_ROOT")")"
117
- headers="$(dashboard_health_headers)" || return 1
118
- printf '%s\n' "$headers" | grep -qi '^x-claude-smart-dashboard:' || return 1
119
- actual_root="$(printf '%s\n' "$headers" | header_value_from "x-claude-smart-plugin-root")"
120
- [ -n "$actual_root" ] || return 1
121
- actual_root="$(normalize_identity_path "$actual_root")"
122
- [ "$actual_root" = "$expected_root" ]
123
- }
124
-
125
84
  # Kill a claude-smart dashboard listener currently holding the port. Gated by
126
- # marker_responds so a foreign app on 3001 is never killed.
85
+ # marker_responds so a foreign listener on the dashboard port is never killed.
127
86
  stop_dashboard_listener() {
128
87
  marker_responds || return 0
129
- command -v lsof >/dev/null 2>&1 || return 0
130
- port_pid=$(lsof -t -i ":$PORT" -sTCP:LISTEN 2>/dev/null | head -n1)
131
- [ -n "$port_pid" ] || return 0
132
- kill -TERM "$port_pid" 2>/dev/null || true
133
- for _ in 1 2 3 4 5; do
134
- kill -0 "$port_pid" 2>/dev/null || return 0
135
- sleep 0.2
136
- done
137
- kill -KILL "$port_pid" 2>/dev/null || true
138
- }
139
-
140
- # True only if *our* dashboard is on the port. Uses plugin-root identity so a
141
- # stale claude-smart listener doesn't cause us to silently skip starting.
142
- is_our_dashboard_running() {
143
- if [ -f "$PID_FILE" ]; then
144
- pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
145
- if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
146
- # PID alive — still verify the port responds with our marker so we
147
- # don't claim "running" when the server crashed but the group leader
148
- # lingered.
149
- if command -v curl >/dev/null 2>&1; then
150
- dashboard_matches_current_root && return 0
151
- else
152
- # No curl — fall back to PID liveness alone.
153
- return 0
154
- fi
155
- fi
156
- fi
157
- # No PID or dead PID — probe the port for our marker (recovers after a
158
- # stale PID file from a crash).
159
- dashboard_matches_current_root && return 0
160
- return 1
88
+ kill_recorded_dashboard
89
+ claude_smart_reap_port_listeners_matching "$PORT" \
90
+ '*next-server*' \
91
+ '*next*start*' \
92
+ '*node*next*' \
93
+ '*claude-smart*dashboard*' || true
161
94
  }
162
95
 
163
96
  # True if *something* is listening on the port, regardless of marker.
@@ -170,6 +103,46 @@ port_occupied() {
170
103
  (echo >"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null
171
104
  }
172
105
 
106
+ resolve_next_bin() {
107
+ if claude_smart_is_windows; then
108
+ for candidate in "$DASHBOARD_DIR/node_modules/.bin/next.cmd" "$DASHBOARD_DIR/node_modules/.bin/next"; do
109
+ [ -f "$candidate" ] && { printf '%s\n' "$candidate"; return 0; }
110
+ done
111
+ else
112
+ for candidate in "$DASHBOARD_DIR/node_modules/.bin/next" "$DASHBOARD_DIR/node_modules/.bin/next.cmd"; do
113
+ [ -f "$candidate" ] && { printf '%s\n' "$candidate"; return 0; }
114
+ done
115
+ fi
116
+ return 1
117
+ }
118
+
119
+ spawn_dashboard() {
120
+ NEXT_BIN="$(resolve_next_bin || true)"
121
+ if [ -z "$NEXT_BIN" ]; then
122
+ reason="local Next.js binary is missing; run claude-smart install/update to restore dashboard dependencies"
123
+ echo "[claude-smart] dashboard: $reason" >>"$LOG_FILE"
124
+ claude_smart_write_dashboard_unavailable "$reason"
125
+ return 1
126
+ fi
127
+ cd "$DASHBOARD_DIR"
128
+ export CLAUDE_SMART_DASHBOARD_WORKSPACE="$WORKSPACE_CWD"
129
+ # Invoke Next directly so custom DASHBOARD_PORT/PORT values are honored
130
+ # without relying on package.json's fixed start script.
131
+ CLAUDE_SMART_SPAWN_KEEP_OUTPUT=1 claude_smart_spawn_detached "$NEXT_BIN" start -p "$PORT" -H 127.0.0.1 >>"$LOG_FILE" 2>&1
132
+ dash_pid=$!
133
+ echo "$dash_pid" > "$PID_FILE"
134
+ }
135
+
136
+ wait_for_dashboard_marker() {
137
+ attempts="$1"
138
+ while [ "$attempts" -gt 0 ]; do
139
+ marker_responds && return 0
140
+ attempts=$((attempts - 1))
141
+ sleep 1
142
+ done
143
+ marker_responds
144
+ }
145
+
173
146
  case "$CMD" in
174
147
  start)
175
148
  if claude_smart_is_internal_invocation_env; then
@@ -181,11 +154,19 @@ case "$CMD" in
181
154
  emit_ok; exit 0
182
155
  fi
183
156
  if [ ! -d "$DASHBOARD_DIR" ]; then emit_ok; exit 0; fi
184
- if is_our_dashboard_running; then claude_smart_clear_dashboard_unavailable; emit_ok; exit 0; fi
185
- if marker_responds; then
186
- echo "[claude-smart] dashboard: stale claude-smart dashboard on port $PORT; restarting from $PLUGIN_ROOT" >>"$LOG_FILE"
187
- stop_dashboard_listener
157
+ if marker_responds; then claude_smart_clear_dashboard_unavailable; emit_ok; exit 0; fi
158
+ if ! claude_smart_service_lock "dashboard"; then
159
+ for _ in 1 2 3 4 5; do
160
+ if marker_responds; then
161
+ claude_smart_clear_dashboard_unavailable
162
+ emit_ok; exit 0
163
+ fi
164
+ sleep 0.2
165
+ done
166
+ emit_ok; exit 0
188
167
  fi
168
+ trap 'claude_smart_service_unlock "dashboard"' EXIT
169
+ if marker_responds; then claude_smart_clear_dashboard_unavailable; emit_ok; exit 0; fi
189
170
  if port_occupied; then
190
171
  echo "[claude-smart] dashboard: port $PORT held by another process; skipping" >>"$LOG_FILE"
191
172
  emit_ok; exit 0
@@ -221,8 +202,6 @@ case "$CMD" in
221
202
  emit_ok; exit 0
222
203
  fi
223
204
 
224
- cd "$DASHBOARD_DIR"
225
-
226
205
  # Detach so the hook returns immediately. claude_smart_spawn_detached
227
206
  # picks the strongest primitive available:
228
207
  # - Linux: setsid (puts child in its own session/group, pid==pgid).
@@ -230,36 +209,41 @@ case "$CMD" in
230
209
  # - Windows: nohup alone (no process groups; tree-kill via taskkill).
231
210
  # Caller-side `>>file 2>&1` redirection is honoured before the child
232
211
  # detaches, so per-OS log paths stay identical.
233
- export CLAUDE_SMART_DASHBOARD_WORKSPACE="$WORKSPACE_CWD"
234
- CLAUDE_SMART_SPAWN_KEEP_OUTPUT=1 claude_smart_spawn_detached "$NPM_BIN" run start >>"$LOG_FILE" 2>&1
235
- dash_pid=$!
236
- # Record the spawned pid, not a pgid sampled with ps. On POSIX,
237
- # setsid/python os.setsid make this pid the new process group leader;
238
- # sampling immediately can race and capture the caller's pgid instead.
239
- # On Windows, claude_smart_kill_tree translates the MSYS pid to WINPID.
240
- echo "$dash_pid" > "$PID_FILE"
241
- dashboard_ready=0
242
- for _ in 1 2 3 4 5; do
243
- if dashboard_matches_current_root; then
244
- dashboard_ready=1
245
- claude_smart_clear_dashboard_unavailable
246
- break
212
+ if ! spawn_dashboard; then
213
+ emit_ok; exit 0
214
+ fi
215
+ if wait_for_dashboard_marker 5; then
216
+ claude_smart_clear_dashboard_unavailable
217
+ emit_ok; exit 0
218
+ fi
219
+ if ! kill -0 "$dash_pid" 2>/dev/null; then
220
+ sleep 2
221
+ if port_occupied; then
222
+ if marker_responds; then
223
+ claude_smart_clear_dashboard_unavailable
224
+ else
225
+ claude_smart_write_dashboard_unavailable "dashboard process exited and port $PORT is now held by a non-claude-smart listener; see $LOG_FILE"
226
+ fi
227
+ else
228
+ echo "[claude-smart] dashboard: first start exited before readiness; retrying once" >>"$LOG_FILE"
229
+ if spawn_dashboard; then
230
+ if wait_for_dashboard_marker 5; then
231
+ claude_smart_clear_dashboard_unavailable
232
+ else
233
+ claude_smart_write_dashboard_unavailable "dashboard process spawned but did not respond on http://127.0.0.1:$PORT within 5s; see $LOG_FILE"
234
+ fi
235
+ fi
247
236
  fi
248
- sleep 1
249
- done
250
- if [ "$dashboard_ready" != "1" ]; then
237
+ else
251
238
  claude_smart_write_dashboard_unavailable "dashboard process spawned but did not respond on http://127.0.0.1:$PORT within 5s; see $LOG_FILE"
252
239
  fi
253
240
  emit_ok
254
241
  ;;
255
242
  stop)
256
- if [ -f "$PID_FILE" ]; then
257
- kill_group "$(cat "$PID_FILE" 2>/dev/null)"
258
- rm -f "$PID_FILE"
259
- fi
243
+ kill_recorded_dashboard
260
244
  # Fallback: if a claude-smart dashboard is still responding on the port (e.g.,
261
245
  # was started outside this script, or the PGID kill missed because
262
- # the process wasn't the group leader) kill whoever owns the port.
246
+ # the process wasn't the group leader) reap only dashboard-shaped listeners.
263
247
  # Gated on the marker header so we never touch a foreign listener.
264
248
  stop_dashboard_listener
265
249
  emit_ok
@@ -269,15 +253,12 @@ case "$CMD" in
269
253
  # interactions/playbooks between sessions. Opt in to teardown by setting
270
254
  # CLAUDE_SMART_DASHBOARD_STOP_ON_END=1 in the environment.
271
255
  if [ "${CLAUDE_SMART_DASHBOARD_STOP_ON_END:-0}" = "1" ]; then
272
- if [ -f "$PID_FILE" ]; then
273
- kill_group "$(cat "$PID_FILE" 2>/dev/null)"
274
- rm -f "$PID_FILE"
275
- fi
256
+ kill_recorded_dashboard
276
257
  fi
277
258
  emit_ok
278
259
  ;;
279
260
  status)
280
- if is_our_dashboard_running; then echo "running on http://localhost:$PORT"; else echo "not running"; fi
261
+ if marker_responds; then echo "running on http://localhost:$PORT"; else echo "not running"; fi
281
262
  ;;
282
263
  *)
283
264
  emit_ok
@@ -44,10 +44,13 @@ claude_smart_source_login_path
44
44
  # Explicit fallback for the astral.sh installer's default paths, in case
45
45
  # the user's login-shell rc hasn't yet been re-sourced to pick them up.
46
46
  claude_smart_prepend_astral_bins
47
+ BACKEND_PORT="${BACKEND_PORT:-8071}"
48
+ export BACKEND_PORT
47
49
  claude_smart_source_reflexio_env
48
50
 
49
51
  PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
50
52
  claude_smart_reexec_stable_plugin_root_if_needed "$PLUGIN_ROOT" "hook_entry.sh" "$@"
53
+ claude_smart_derive_reflexio_url_from_backend_port
51
54
 
52
55
  FAILURE_MARKER="$HOME/.claude-smart/install-failed"
53
56
  STATE_DIR="$HOME/.claude-smart"
@@ -149,7 +149,21 @@ install_vendored_reflexio() {
149
149
  local VENDORED_REFLEXIO PLUGIN_PYTHON
150
150
 
151
151
  VENDORED_REFLEXIO="$PLUGIN_ROOT/vendor/reflexio"
152
- [ -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
153
167
 
154
168
  if claude_smart_is_windows; then
155
169
  PLUGIN_PYTHON="$PLUGIN_ROOT/.venv/Scripts/python.exe"
@@ -2185,6 +2185,20 @@ def cmd_restart(args: argparse.Namespace) -> int:
2185
2185
  return 0
2186
2186
 
2187
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
+
2188
2202
  sys.stdout.write("Stopping reflexio backend…\n")
2189
2203
  _run_service(_BACKEND_SCRIPT, "stop")
2190
2204
  if do_dashboard:
@@ -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
@@ -29,7 +29,10 @@ DEFAULT_CLAUDE_SMART_HOST = runtime.HOST_CLAUDE_CODE
29
29
 
30
30
  _LOCAL_DEFAULT_ENTRIES = (
31
31
  (
32
- "# Route reflexio generation through the configured local host 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.",
33
36
  CLAUDE_SMART_USE_LOCAL_CLI_ENV,
34
37
  "1",
35
38
  ),
@@ -6,7 +6,7 @@ import re
6
6
  import time
7
7
  from typing import Any
8
8
 
9
- from claude_smart import state
9
+ from claude_smart import runtime, state
10
10
 
11
11
  # Tool inputs are persisted locally and later published to reflexio, so we
12
12
  # apply a conservative redaction pass at ingestion time. Chosen to avoid
@@ -144,5 +144,6 @@ def handle(payload: dict[str, Any]) -> None:
144
144
  "tool_input": _redact(payload.get("tool_input") or {}),
145
145
  "tool_output": _redact_string(output_text) if output_text else "",
146
146
  "status": _derive_status(tool_response),
147
+ "host": runtime.attribution_host(),
147
148
  }
148
149
  state.append(session_id, record)
@@ -21,7 +21,7 @@ import time
21
21
  from pathlib import Path
22
22
  from typing import Any
23
23
 
24
- from claude_smart import env_config, ids, publish, state
24
+ from claude_smart import env_config, ids, publish, runtime, state
25
25
  from claude_smart.events.stop import (
26
26
  _read_transcript_entries,
27
27
  _scan_transcript_for_assistant_text,
@@ -119,6 +119,7 @@ def _maybe_synthesize_assistant_anchor(
119
119
  "content": assistant_text,
120
120
  "user_id": project_id,
121
121
  "synthesised_by": "session_end_anchor",
122
+ "host": runtime.attribution_host(),
122
123
  },
123
124
  )
124
125
  _LOGGER.info(
@@ -408,6 +408,7 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
408
408
  "role": "User",
409
409
  "content": decision_text,
410
410
  "user_id": project_id,
411
+ "host": runtime.attribution_host(),
411
412
  },
412
413
  )
413
414
  elif prompt and not _has_unpublished_user_turn(session_id):
@@ -418,6 +419,7 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
418
419
  "role": "User",
419
420
  "content": prompt,
420
421
  "user_id": project_id,
422
+ "host": runtime.attribution_host(),
421
423
  },
422
424
  )
423
425
 
@@ -426,6 +428,7 @@ def handle(payload: dict[str, Any]) -> tuple[publish.PublishStatus, int] | None:
426
428
  "role": "Assistant",
427
429
  "content": assistant_text,
428
430
  "user_id": project_id,
431
+ "host": runtime.attribution_host(),
429
432
  }
430
433
  if cited_items:
431
434
  record["cited_items"] = cited_items
@@ -28,7 +28,7 @@ import logging
28
28
  import time
29
29
  from typing import Any
30
30
 
31
- from claude_smart import context_inject, ids, state
31
+ from claude_smart import context_inject, ids, runtime, state
32
32
 
33
33
  _LOGGER = logging.getLogger(__name__)
34
34
  _TOP_K = 3
@@ -58,6 +58,7 @@ def handle(payload: dict[str, Any]) -> None:
58
58
  "role": "User",
59
59
  "content": prompt,
60
60
  "user_id": project_id,
61
+ "host": runtime.attribution_host(),
61
62
  },
62
63
  )
63
64
 
@@ -20,7 +20,8 @@ Two distinct sources of unwanted hook fires:
20
20
 
21
21
  Detection signals, OR'd:
22
22
  - ``CLAUDE_CODE_ENTRYPOINT`` is anything other than an interactive entrypoint
23
- (``"cli"`` or Claude Desktop's ``"claude-desktop"``). Headless
23
+ (``"cli"``, Claude Desktop's ``"claude-desktop"``, or Claude Code IDE
24
+ entrypoints such as ``"claude-vscode"``/``"claude-jetbrains"``). Headless
24
25
  ``claude -p`` sets ``sdk-cli`` (and the SDKs may set other values). This
25
26
  catches case (2) for any third-party tool, not just claude-mem.
26
27
  - Env var ``CLAUDE_SMART_INTERNAL=1``, set by reflexio's provider
@@ -46,7 +47,9 @@ from typing import Any
46
47
  from claude_smart import runtime
47
48
 
48
49
  _ENTRYPOINT_VAR = "CLAUDE_CODE_ENTRYPOINT"
49
- _INTERACTIVE_ENTRYPOINTS = frozenset({"cli", "claude-desktop"})
50
+ _INTERACTIVE_ENTRYPOINTS = frozenset(
51
+ {"cli", "claude-desktop", "claude-vscode", "claude-jetbrains"}
52
+ )
50
53
  _CODEX_TITLE_PROMPT_PREFIX = (
51
54
  "You are a helpful assistant. You will be presented with a user prompt, "
52
55
  "and your job is to provide a short title for a task"
@@ -4,6 +4,13 @@ Reflexio's ``LocalScriptAssistant`` sends one JSON payload on stdin and expects
4
4
  one JSON object on stdout. This module bridges that protocol to a guarded
5
5
  local CLI subprocess so candidate playbooks can be evaluated against the active
6
6
  host without re-entering claude-smart/reflexio hooks.
7
+
8
+ Host CLI resolution: when ``CLAUDE_SMART_CLI_PATH`` is set in the environment
9
+ (``backend-service.sh`` writes it for ``opencode`` and ``codex`` hosts so
10
+ generation routes through the host-specific compatibility bridge instead of
11
+ the real Claude CLI), this script honours that override. Falling back to
12
+ ``shutil.which("claude")`` preserves the legacy Claude Code host behaviour
13
+ where no override is configured.
7
14
  """
8
15
 
9
16
  from __future__ import annotations
@@ -22,6 +29,20 @@ from claude_smart import internal_call, runtime
22
29
  _CLI_TIMEOUT_SECONDS = 300
23
30
  _READ_ONLY_TOOLS = "Read,Grep,Glob,LS"
24
31
  _MUTATING_TOOLS = "Bash,Edit,Write,MultiEdit,NotebookEdit"
32
+ # Mirrors ``reflexio.server.llm.providers.claude_code_provider._ENV_CLI_PATH``
33
+ # so backend-service.sh can route both the reflexio Claude CLI provider and
34
+ # this assistant script through the same host-specific bridge.
35
+ _ENV_CLI_PATH = "CLAUDE_SMART_CLI_PATH"
36
+ _CLAUDE_SMART_COMPAT_BRIDGE_NAMES = frozenset(
37
+ {
38
+ "codex-claude-compat",
39
+ "codex-claude-compat.cmd",
40
+ "codex-claude-compat.js",
41
+ "opencode-claude-compat",
42
+ "opencode-claude-compat.cmd",
43
+ "opencode-claude-compat.js",
44
+ }
45
+ )
25
46
 
26
47
 
27
48
  class OptimizerAssistantError(Exception):
@@ -142,26 +163,51 @@ def _run_local_cli(*, prompt: str, system_prompt: str) -> str:
142
163
  return _run_claude(prompt=prompt, system_prompt=system_prompt)
143
164
 
144
165
 
166
+ def _resolve_claude_cli_path() -> str:
167
+ """Return the host CLI to invoke for evaluation rollouts.
168
+
169
+ When ``CLAUDE_SMART_CLI_PATH`` is set, prefer it so backend-service.sh can
170
+ route the assistant through the same bridge it uses for the reflexio
171
+ ``claude-code`` provider (e.g. ``opencode-claude-compat`` for the OpenCode
172
+ host). Falls back to ``shutil.which("claude")`` so Claude Code hosts — and
173
+ any environment that never set the override — continue to use the real
174
+ ``claude`` CLI.
175
+ """
176
+ override = os.environ.get(_ENV_CLI_PATH)
177
+ if override:
178
+ return override
179
+ return shutil.which("claude") or "claude"
180
+
181
+
182
+ def _is_claude_smart_compat_bridge(cli_path: str) -> bool:
183
+ return Path(cli_path).name.lower() in _CLAUDE_SMART_COMPAT_BRIDGE_NAMES
184
+
185
+
145
186
  def _run_claude(*, prompt: str, system_prompt: str) -> str:
146
- cli_path = shutil.which("claude") or "claude"
147
- # This is an evaluation rollout, not a real user session: allow local
148
- # inspection, but prevent filesystem, shell, MCP, and session mutations.
187
+ cli_path = _resolve_claude_cli_path()
149
188
  cmd = [
150
189
  cli_path,
151
190
  "-p",
152
191
  "--output-format",
153
192
  "json",
154
- "--permission-mode",
155
- "plan",
156
- "--tools",
157
- _READ_ONLY_TOOLS,
158
- "--disallowedTools",
159
- _MUTATING_TOOLS,
160
- "--no-session-persistence",
161
- "--mcp-config",
162
- '{"mcpServers": {}}',
163
- "--strict-mcp-config",
164
193
  ]
194
+ if not _is_claude_smart_compat_bridge(cli_path):
195
+ # This is an evaluation rollout, not a real user session: allow local
196
+ # inspection, but prevent filesystem, shell, MCP, and session mutations.
197
+ cmd.extend(
198
+ [
199
+ "--permission-mode",
200
+ "plan",
201
+ "--tools",
202
+ _READ_ONLY_TOOLS,
203
+ "--disallowedTools",
204
+ _MUTATING_TOOLS,
205
+ "--no-session-persistence",
206
+ "--mcp-config",
207
+ '{"mcpServers": {}}',
208
+ "--strict-mcp-config",
209
+ ]
210
+ )
165
211
  if system_prompt:
166
212
  cmd.extend(["--append-system-prompt", system_prompt])
167
213