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
@@ -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,128 +51,98 @@ 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.
164
97
  port_occupied() {
165
98
  if command -v curl >/dev/null 2>&1; then
166
- curl -sf -o /dev/null "http://127.0.0.1:$PORT" 2>/dev/null && return 0
99
+ curl -sf --max-time 2 -o /dev/null "http://127.0.0.1:$PORT" 2>/dev/null && return 0
167
100
  # curl with -sfI against a 404/405 still indicates "something answered".
168
101
  # Use a connect-only probe as a secondary signal.
169
102
  fi
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
@@ -8,6 +8,61 @@
8
8
  # invalid target
9
9
  set -eu
10
10
 
11
+ HERE="$(cd "$(dirname "$0")" && pwd)"
12
+ if [ -f "$HERE/_lib.sh" ]; then
13
+ # shellcheck source=_lib.sh
14
+ . "$HERE/_lib.sh"
15
+ fi
16
+
17
+ # This script can be invoked without plugin/scripts/_lib.sh; keep these fallbacks
18
+ # in sync with the shared helpers there.
19
+ if ! command -v claude_smart_is_windows >/dev/null 2>&1; then
20
+ claude_smart_is_windows() {
21
+ case "$(uname -s 2>/dev/null)" in
22
+ MINGW*|MSYS*|CYGWIN*) return 0 ;;
23
+ *) return 1 ;;
24
+ esac
25
+ }
26
+ fi
27
+
28
+ if ! command -v claude_smart_to_windows_path >/dev/null 2>&1; then
29
+ claude_smart_to_windows_path() {
30
+ path="$1"
31
+ if command -v cygpath >/dev/null 2>&1; then
32
+ cygpath -w "$path"
33
+ return $?
34
+ fi
35
+ printf '%s\n' "$path" | awk '
36
+ function slash_to_backslash(value) {
37
+ gsub(/\//, "\\", value)
38
+ return value
39
+ }
40
+ function drive_path(drive, rest) {
41
+ drive = toupper(drive)
42
+ sub(/^\//, "", rest)
43
+ if (rest == "") {
44
+ return drive ":\\"
45
+ }
46
+ return drive ":\\" slash_to_backslash(rest)
47
+ }
48
+ {
49
+ normalized = $0
50
+ gsub(/\\/, "/", normalized)
51
+ if (normalized ~ /^[A-Za-z]:($|\/)/) {
52
+ print drive_path(substr(normalized, 1, 1), substr(normalized, 3))
53
+ } else if (normalized ~ /^\/cygdrive\/[A-Za-z]($|\/)/) {
54
+ print drive_path(substr(normalized, 11, 1), substr(normalized, 12))
55
+ } else if (normalized ~ /^\/mnt\/[A-Za-z]($|\/)/) {
56
+ print drive_path(substr(normalized, 6, 1), substr(normalized, 7))
57
+ } else if (normalized ~ /^\/[A-Za-z]($|\/)/) {
58
+ print drive_path(substr(normalized, 2, 1), substr(normalized, 3))
59
+ } else {
60
+ print slash_to_backslash(normalized)
61
+ }
62
+ }'
63
+ }
64
+ fi
65
+
11
66
  TARGET="${1:-}"
12
67
  FORCE="${2:-}"
13
68
 
@@ -25,9 +80,46 @@ TARGET="$(cd "$TARGET" && pwd -P)"
25
80
  LINK="$HOME/.reflexio/plugin-root"
26
81
  mkdir -p "$(dirname "$LINK")"
27
82
 
28
- if [ "$FORCE" = "--force" ]; then
83
+ write_plugin_root_metadata() {
84
+ printf '%s\n' "$TARGET" >"$HOME/.reflexio/plugin-root.txt"
85
+ }
86
+
87
+ write_plugin_root_link() {
88
+ reason="$1"
89
+ if claude_smart_is_windows; then
90
+ link_win="$(claude_smart_to_windows_path "$LINK")"
91
+ target_win="$(claude_smart_to_windows_path "$TARGET")"
92
+ if [ -L "$LINK" ] || [ -f "$LINK" ]; then
93
+ rm -f "$LINK"
94
+ elif [ -e "$LINK" ]; then
95
+ # Intentionally avoid rmdir //S //Q here: if this is a real
96
+ # directory rather than a junction, preserve it and use metadata
97
+ # fallback instead of recursively deleting user-owned contents.
98
+ if ! cmd.exe //C rmdir "$link_win" >/dev/null 2>&1; then
99
+ echo "[claude-smart] ensure-plugin-root: could not remove existing Windows junction/path at $LINK" >&2
100
+ fi
101
+ fi
102
+ if [ ! -e "$LINK" ] && cmd.exe //C mklink //J "$link_win" "$target_win" >/dev/null 2>&1; then
103
+ write_plugin_root_metadata
104
+ echo "[claude-smart] plugin-root → $TARGET${reason:+ ($reason)}" >&2
105
+ return 0
106
+ fi
107
+ write_plugin_root_metadata
108
+ if [ -e "$LINK" ]; then
109
+ echo "[claude-smart] ensure-plugin-root: $LINK blocks Windows junction; preserving occupied path and wrote plugin-root.txt fallback" >&2
110
+ else
111
+ echo "[claude-smart] ensure-plugin-root: mklink //J failed; wrote plugin-root.txt fallback" >&2
112
+ fi
113
+ echo "[claude-smart] plugin-root.txt → $TARGET${reason:+ ($reason; junction unavailable)}" >&2
114
+ return 0
115
+ fi
116
+
29
117
  ln -sfn "$TARGET" "$LINK"
30
- echo "[claude-smart] plugin-root → $TARGET (forced)" >&2
118
+ echo "[claude-smart] plugin-root → $TARGET${reason:+ ($reason)}" >&2
119
+ }
120
+
121
+ if [ "$FORCE" = "--force" ]; then
122
+ write_plugin_root_link "forced"
31
123
  exit 0
32
124
  fi
33
125
 
@@ -43,8 +135,7 @@ if [ -z "$FOLLOW" ] && [ -f "$HOME/.claude-smart/.env" ]; then
43
135
  FOLLOW="${FOLLOW#\'}"; FOLLOW="${FOLLOW%\'}"
44
136
  fi
45
137
  if [ "$FOLLOW" = "1" ]; then
46
- ln -sfn "$TARGET" "$LINK"
47
- echo "[claude-smart] plugin-root → $TARGET (follow-session)" >&2
138
+ write_plugin_root_link "follow-session"
48
139
  exit 0
49
140
  fi
50
141
 
@@ -53,16 +144,24 @@ fi
53
144
  # always retarget it to $TARGET. Plugin updates leave old version
54
145
  # directories behind, so a valid pyproject.toml at the stale target is
55
146
  # not proof the link is fresh.
147
+ CURRENT=""
56
148
  if [ -L "$LINK" ]; then
57
149
  # Literal target string, not realpath: we compare against what was written by ln -s.
58
150
  CURRENT="$(readlink "$LINK" 2>/dev/null || true)"
151
+ elif claude_smart_is_windows && [ -e "$LINK" ] && [ -f "$HOME/.reflexio/plugin-root.txt" ]; then
152
+ # Windows junctions are not reported as symlinks by Git Bash, so use the
153
+ # metadata written alongside successful junction creation.
154
+ CURRENT="$(cat "$HOME/.reflexio/plugin-root.txt" 2>/dev/null || true)"
155
+ elif claude_smart_is_windows && [ -e "$LINK" ]; then
156
+ echo "[claude-smart] ensure-plugin-root: $LINK is not a symlink and plugin-root.txt is missing; cannot determine whether the Windows plugin-root tracks the active plugin. Run ensure-plugin-root.sh with --force or delete the occupied path to recreate it." >&2
157
+ fi
158
+ if [ -n "$CURRENT" ]; then
59
159
  case "$CURRENT" in
60
160
  "$HOME/.claude/plugins/cache/"*|"$HOME/.codex/plugins/cache/"*)
61
161
  CURRENT_NORM="${CURRENT%/}"
62
162
  TARGET_NORM="${TARGET%/}"
63
163
  if [ "$CURRENT_NORM" != "$TARGET_NORM" ]; then
64
- ln -sfn "$TARGET" "$LINK"
65
- echo "[claude-smart] plugin-root → $TARGET (cache-tracking, was $CURRENT)" >&2
164
+ write_plugin_root_link "cache-tracking, was $CURRENT"
66
165
  fi
67
166
  exit 0
68
167
  ;;
@@ -75,5 +174,4 @@ if [ -f "$LINK/pyproject.toml" ]; then
75
174
  exit 0
76
175
  fi
77
176
 
78
- ln -sfn "$TARGET" "$LINK"
79
- echo "[claude-smart] plugin-root → $TARGET" >&2
177
+ write_plugin_root_link ""
@@ -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"
@@ -193,7 +193,14 @@ function parseOpenCodeJson(stdout) {
193
193
  chunks.push(event.result);
194
194
  }
195
195
  }
196
- return chunks.join("").trim();
196
+ return stripOuterMarkdownFence(chunks.join("").trim());
197
+ }
198
+
199
+ function stripOuterMarkdownFence(content) {
200
+ // OpenCode can wrap one-shot JSON in a whole-response markdown fence.
201
+ // Strip only that outer fence so streamed and one-shot output normalize alike.
202
+ const match = String(content || "").match(/^```[^\r\n]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/);
203
+ return match ? match[1].trim() : content;
197
204
  }
198
205
 
199
206
  function timeoutMs() {