claude-smart 0.2.31 → 0.2.33

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 (60) hide show
  1. package/README.md +2 -2
  2. package/bin/claude-smart.js +222 -20
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/dashboard/app/api/config/route.ts +11 -2
  7. package/plugin/dashboard/app/api/health/route.ts +45 -6
  8. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +15 -7
  9. package/plugin/dashboard/app/api/rules/applied/route.ts +27 -0
  10. package/plugin/dashboard/app/configure/env/page.tsx +36 -31
  11. package/plugin/dashboard/app/configure/layout.tsx +1 -1
  12. package/plugin/dashboard/app/configure/server/page.tsx +8 -14
  13. package/plugin/dashboard/app/dashboard/page.tsx +311 -115
  14. package/plugin/dashboard/app/globals.css +80 -66
  15. package/plugin/dashboard/app/layout.tsx +13 -10
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +21 -32
  17. package/plugin/dashboard/app/preferences/page.tsx +154 -54
  18. package/plugin/dashboard/app/preferences/project/[id]/page.tsx +1 -0
  19. package/plugin/dashboard/app/rules/[id]/page.tsx +51 -0
  20. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +4 -4
  21. package/plugin/dashboard/app/sessions/page.tsx +14 -10
  22. package/plugin/dashboard/app/skills/page.tsx +175 -56
  23. package/plugin/dashboard/app/skills/project/[id]/page.tsx +22 -38
  24. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +20 -37
  25. package/plugin/dashboard/components/common/delete-learning-danger-zone.tsx +166 -0
  26. package/plugin/dashboard/components/common/empty-state.tsx +4 -2
  27. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  28. package/plugin/dashboard/components/common/page-header.tsx +5 -3
  29. package/plugin/dashboard/components/common/page-tabs.tsx +4 -4
  30. package/plugin/dashboard/components/common/stat-card.tsx +9 -5
  31. package/plugin/dashboard/components/layout/sidebar.tsx +24 -9
  32. package/plugin/dashboard/components/layout/top-bar.tsx +37 -25
  33. package/plugin/dashboard/components/ui/input.tsx +1 -0
  34. package/plugin/dashboard/hooks/use-settings.tsx +30 -61
  35. package/plugin/dashboard/hooks/use-stall-state.ts +5 -9
  36. package/plugin/dashboard/lib/config-file.ts +4 -0
  37. package/plugin/dashboard/lib/reflexio-client.ts +23 -48
  38. package/plugin/dashboard/lib/session-reader.ts +222 -6
  39. package/plugin/dashboard/lib/types.ts +21 -1
  40. package/plugin/dashboard/package-lock.json +70 -95
  41. package/plugin/dashboard/package.json +5 -2
  42. package/plugin/pyproject.toml +1 -1
  43. package/plugin/scripts/_lib.sh +61 -0
  44. package/plugin/scripts/backend-service.sh +13 -2
  45. package/plugin/scripts/cli.sh +1 -0
  46. package/plugin/scripts/codex-hook.js +101 -3
  47. package/plugin/scripts/dashboard-service.sh +95 -19
  48. package/plugin/scripts/hook_entry.sh +13 -7
  49. package/plugin/scripts/smart-install.sh +18 -13
  50. package/plugin/src/claude_smart/cli.py +138 -24
  51. package/plugin/src/claude_smart/context_format.py +244 -6
  52. package/plugin/src/claude_smart/context_inject.py +8 -1
  53. package/plugin/src/claude_smart/cs_cite.py +186 -34
  54. package/plugin/src/claude_smart/env_config.py +103 -0
  55. package/plugin/src/claude_smart/events/stop.py +32 -6
  56. package/plugin/src/claude_smart/ids.py +32 -0
  57. package/plugin/src/claude_smart/internal_call.py +30 -0
  58. package/plugin/src/claude_smart/publish.py +8 -3
  59. package/plugin/src/claude_smart/reflexio_adapter.py +106 -29
  60. package/plugin/uv.lock +1 -1
@@ -32,6 +32,59 @@ claude_smart_prepend_node_bins() {
32
32
  export PATH="$_CS_NODE_ROOT/bin:$_CS_NODE_ROOT:$PATH"
33
33
  }
34
34
 
35
+ claude_smart_env_unquote() {
36
+ local value first last
37
+ value="$1"
38
+ first="${value%"${value#?}"}"
39
+ last="${value#"${value%?}"}"
40
+ if { [ "$first" = '"' ] && [ "$last" = '"' ]; } || { [ "$first" = "'" ] && [ "$last" = "'" ]; }; then
41
+ value="${value#?}"
42
+ value="${value%?}"
43
+ fi
44
+ printf '%s\n' "$value"
45
+ }
46
+
47
+ claude_smart_source_reflexio_env() {
48
+ local env_file line key value
49
+ env_file="$HOME/.reflexio/.env"
50
+ [ -f "$env_file" ] || return 0
51
+ while IFS= read -r line || [ -n "$line" ]; do
52
+ line="${line#"${line%%[![:space:]]*}"}"
53
+ line="${line%"${line##*[![:space:]]}"}"
54
+ [ -n "$line" ] || continue
55
+ case "$line" in
56
+ \#*) continue ;;
57
+ export\ *) line="${line#export }" ;;
58
+ esac
59
+ key="${line%%=*}"
60
+ [ "$key" != "$line" ] || continue
61
+ value="${line#*=}"
62
+ key="${key%"${key##*[![:space:]]}"}"
63
+ value="${value#"${value%%[![:space:]]*}"}"
64
+ value="${value%"${value##*[![:space:]]}"}"
65
+ case "$key" in
66
+ REFLEXIO_URL|REFLEXIO_API_KEY|REFLEXIO_USER_ID|CLAUDE_SMART_USE_LOCAL_CLI|CLAUDE_SMART_USE_LOCAL_EMBEDDING|CLAUDE_SMART_BACKEND_AUTOSTART|CLAUDE_SMART_DASHBOARD_AUTOSTART|CLAUDE_SMART_CLI_PATH|CLAUDE_SMART_CLI_TIMEOUT|CLAUDE_SMART_STATE_DIR|CLAUDE_SMART_ENABLE_OPTIMIZER)
67
+ if [ -z "$(eval "printf '%s' \"\${$key:-}\"")" ]; then
68
+ value="$(claude_smart_env_unquote "$value")"
69
+ export "$key=$value"
70
+ fi
71
+ ;;
72
+ esac
73
+ done < "$env_file"
74
+ }
75
+
76
+ claude_smart_reflexio_url_is_remote() {
77
+ local url
78
+ url="${REFLEXIO_URL:-}"
79
+ [ -n "$url" ] || return 1
80
+ case "$url" in
81
+ http://localhost|http://localhost/|http://localhost:*|http://127.0.0.1|http://127.0.0.1/|http://127.0.0.1:*|http://0.0.0.0|http://0.0.0.0/|http://0.0.0.0:*|http://\[::1\]|http://\[::1\]/|http://\[::1\]:*)
82
+ return 1
83
+ ;;
84
+ esac
85
+ return 0
86
+ }
87
+
35
88
  claude_smart_is_internal_invocation_env() {
36
89
  if [ "${CLAUDE_SMART_INTERNAL:-}" = "1" ]; then
37
90
  return 0
@@ -42,6 +95,14 @@ claude_smart_is_internal_invocation_env() {
42
95
  return 1
43
96
  }
44
97
 
98
+ claude_smart_emit_continue() {
99
+ if [ "${CLAUDE_SMART_HOST:-claude-code}" = "codex" ]; then
100
+ echo '{"continue":true}'
101
+ else
102
+ echo '{"continue":true,"suppressOutput":true}'
103
+ fi
104
+ }
105
+
45
106
  claude_smart_dashboard_unavailable_marker() {
46
107
  printf '%s\n' "$HOME/.claude-smart/dashboard-unavailable"
47
108
  }
@@ -22,6 +22,7 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
22
22
  . "$HERE/_lib.sh"
23
23
  claude_smart_source_login_path
24
24
  claude_smart_prepend_astral_bins
25
+ claude_smart_source_reflexio_env
25
26
 
26
27
  CMD="${1:-start}"
27
28
  PORT=8071
@@ -67,7 +68,7 @@ LOG_MAX_BYTES="$(claude_smart_log_max_bytes)"
67
68
  mkdir -p "$STATE_DIR"
68
69
  claude_smart_trim_log_file "$LOG_FILE" "$LOG_MAX_BYTES"
69
70
 
70
- emit_ok() { echo '{"continue":true,"suppressOutput":true}'; }
71
+ emit_ok() { claude_smart_emit_continue; }
71
72
 
72
73
  emit_start_failure() {
73
74
  reason="$1"
@@ -182,6 +183,10 @@ case "$CMD" in
182
183
  if claude_smart_is_internal_invocation_env; then
183
184
  emit_ok; exit 0
184
185
  fi
186
+ if claude_smart_reflexio_url_is_remote; then
187
+ claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: remote REFLEXIO_URL configured; skipping local backend start"
188
+ emit_ok; exit 0
189
+ fi
185
190
  # Opt-out: users who don't want the backend managed by the hook can
186
191
  # set CLAUDE_SMART_BACKEND_AUTOSTART=0.
187
192
  if [ "${CLAUDE_SMART_BACKEND_AUTOSTART:-1}" = "0" ]; then
@@ -269,7 +274,13 @@ case "$CMD" in
269
274
  emit_ok
270
275
  ;;
271
276
  status)
272
- if is_our_backend_running; then echo "running on http://localhost:$PORT"; else echo "not running"; fi
277
+ if claude_smart_reflexio_url_is_remote; then
278
+ echo "remote configured at $REFLEXIO_URL"
279
+ elif is_our_backend_running; then
280
+ echo "running on http://localhost:$PORT"
281
+ else
282
+ echo "not running"
283
+ fi
273
284
  ;;
274
285
  *)
275
286
  emit_ok
@@ -15,6 +15,7 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
15
15
  claude_smart_source_login_path
16
16
  claude_smart_prepend_astral_bins
17
17
  claude_smart_prepend_node_bins
18
+ claude_smart_source_reflexio_env
18
19
 
19
20
  PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
20
21
 
@@ -16,7 +16,46 @@ const DASHBOARD_PORT = 3001;
16
16
  const LOG_MAX_BYTES = 10000000;
17
17
 
18
18
  function emitOk() {
19
- process.stdout.write('{"continue":true,"suppressOutput":true}\n');
19
+ process.stdout.write('{"continue":true}\n');
20
+ }
21
+
22
+ function emitHookOk() {
23
+ process.stdout.write('{"continue":true}\n');
24
+ }
25
+
26
+ function emitNormalizedHookOutput(stdout) {
27
+ const lines = String(stdout || "")
28
+ .split(/\r?\n/)
29
+ .map((line) => line.trim())
30
+ .filter(Boolean);
31
+ if (lines.length === 0) {
32
+ emitHookOk();
33
+ return;
34
+ }
35
+
36
+ const merged = {};
37
+ for (const line of lines) {
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(line);
41
+ } catch {
42
+ appendLog("backend.log", `[claude-smart] codex hook emitted non-JSON stdout: ${line.slice(0, 500)}`);
43
+ emitHookOk();
44
+ return;
45
+ }
46
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
47
+ appendLog("backend.log", `[claude-smart] codex hook emitted non-object JSON stdout: ${line.slice(0, 500)}`);
48
+ emitHookOk();
49
+ return;
50
+ }
51
+ Object.assign(merged, parsed);
52
+ }
53
+
54
+ if (!Object.prototype.hasOwnProperty.call(merged, "continue")) {
55
+ merged.continue = true;
56
+ }
57
+ delete merged.suppressOutput;
58
+ process.stdout.write(`${JSON.stringify(merged)}\n`);
20
59
  }
21
60
 
22
61
  function ensureDir(dir) {
@@ -121,6 +160,57 @@ function writeBackendUrl(port) {
121
160
  fs.writeFileSync(backendUrlFile(), `http://localhost:${port}/\n`);
122
161
  }
123
162
 
163
+ function unquoteEnvValue(value) {
164
+ const trimmed = String(value || "").trim();
165
+ if (trimmed.length >= 2) {
166
+ const first = trimmed[0];
167
+ const last = trimmed[trimmed.length - 1];
168
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
169
+ return trimmed.slice(1, -1);
170
+ }
171
+ }
172
+ return trimmed;
173
+ }
174
+
175
+ function loadReflexioEnv() {
176
+ const file = path.join(REFLEXIO_DIR, ".env");
177
+ let text;
178
+ try {
179
+ text = fs.readFileSync(file, "utf8");
180
+ } catch {
181
+ return;
182
+ }
183
+ const allowed = new Set([
184
+ "REFLEXIO_URL",
185
+ "REFLEXIO_API_KEY",
186
+ "REFLEXIO_USER_ID",
187
+ "CLAUDE_SMART_USE_LOCAL_CLI",
188
+ "CLAUDE_SMART_USE_LOCAL_EMBEDDING",
189
+ "CLAUDE_SMART_BACKEND_AUTOSTART",
190
+ "CLAUDE_SMART_DASHBOARD_AUTOSTART",
191
+ "CLAUDE_SMART_CLI_PATH",
192
+ "CLAUDE_SMART_CLI_TIMEOUT",
193
+ "CLAUDE_SMART_STATE_DIR",
194
+ "CLAUDE_SMART_ENABLE_OPTIMIZER",
195
+ ]);
196
+ for (const rawLine of text.split(/\r?\n/)) {
197
+ let line = rawLine.trim();
198
+ if (!line || line.startsWith("#")) continue;
199
+ if (line.startsWith("export ")) line = line.slice("export ".length).trimStart();
200
+ const eq = line.indexOf("=");
201
+ if (eq < 0) continue;
202
+ const key = line.slice(0, eq).trim();
203
+ if (!allowed.has(key) || process.env[key]) continue;
204
+ process.env[key] = unquoteEnvValue(line.slice(eq + 1));
205
+ }
206
+ }
207
+
208
+ function reflexioUrlIsRemote() {
209
+ const url = process.env.REFLEXIO_URL || "";
210
+ if (!url) return false;
211
+ return !/^http:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?\/?$/i.test(url);
212
+ }
213
+
124
214
  function codexCompatPath(root) {
125
215
  const filename = process.platform === "win32"
126
216
  ? "codex-claude-compat.cmd"
@@ -294,6 +384,11 @@ async function startBackend(root) {
294
384
  emitOk();
295
385
  return;
296
386
  }
387
+ if (reflexioUrlIsRemote()) {
388
+ appendLog("backend.log", "[claude-smart] backend: remote REFLEXIO_URL configured; skipping local backend start");
389
+ emitOk();
390
+ return;
391
+ }
297
392
  const pidFile = path.join(STATE_DIR, "backend.pid");
298
393
  for (const port of [DEFAULT_BACKEND_PORT, FALLBACK_BACKEND_PORT]) {
299
394
  if (pidAlive(readPid(pidFile)) && await healthOk(port, "/health")) {
@@ -419,7 +514,7 @@ function runHook(root, event) {
419
514
  }
420
515
  if (!uv) {
421
516
  appendLog("backend.log", "[claude-smart] hook: uv not on PATH after installer; skipping");
422
- emitOk();
517
+ emitHookOk();
423
518
  return 0;
424
519
  }
425
520
  }
@@ -434,17 +529,20 @@ function runHook(root, event) {
434
529
  REFLEXIO_URL: readBackendUrl(),
435
530
  CLAUDE_SMART_HOST: "codex",
436
531
  CLAUDE_SMART_CLI_PATH: process.env.CLAUDE_SMART_CLI_PATH || codexCompatPath(root),
532
+ CLAUDE_SMART_CITATION_LINK_STYLE: process.env.CLAUDE_SMART_CITATION_LINK_STYLE || "osc8",
437
533
  },
438
534
  input,
439
- stdio: ["pipe", "inherit", "inherit"],
535
+ stdio: ["pipe", "pipe", "inherit"],
440
536
  windowsHide: true,
441
537
  },
442
538
  );
539
+ emitNormalizedHookOutput(result.stdout);
443
540
  return typeof result.status === "number" ? result.status : 1;
444
541
  }
445
542
 
446
543
  async function main() {
447
544
  prependRuntimePath();
545
+ loadReflexioEnv();
448
546
  const root = pluginRoot();
449
547
  process.env.PLUGIN_ROOT = root;
450
548
  process.env.CLAUDE_PLUGIN_ROOT = root;
@@ -23,6 +23,7 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
23
23
  . "$HERE/_lib.sh"
24
24
  claude_smart_source_login_path
25
25
  claude_smart_prepend_node_bins
26
+ claude_smart_source_reflexio_env
26
27
 
27
28
  CMD="${1:-start}"
28
29
  PORT=3001
@@ -36,7 +37,7 @@ PID_FILE="$STATE_DIR/dashboard.pid"
36
37
  LOG_FILE="$STATE_DIR/dashboard.log"
37
38
  mkdir -p "$STATE_DIR"
38
39
 
39
- emit_ok() { echo '{"continue":true,"suppressOutput":true}'; }
40
+ emit_ok() { claude_smart_emit_continue; }
40
41
 
41
42
  # Tree-kill the recorded process. Delegates to claude_smart_kill_tree
42
43
  # (POSIX: signal the process group; Windows: taskkill /T /F /PID).
@@ -45,15 +46,96 @@ kill_group() {
45
46
  }
46
47
 
47
48
  # True if the marker header served by app/api/health is present on the
48
- # port. Requires curl — absence is reported as false.
49
+ # port. Requires curl — absence is reported as false. This deliberately
50
+ # accepts any claude-smart dashboard, including stale cache versions, so
51
+ # stop/uninstall can safely reap them without touching foreign listeners.
49
52
  marker_responds() {
50
53
  command -v curl >/dev/null 2>&1 || return 1
51
54
  curl -sfI "http://127.0.0.1:$PORT/api/health" 2>/dev/null \
52
55
  | grep -qi '^x-claude-smart-dashboard:'
53
56
  }
54
57
 
55
- # True only if *our* dashboard is on the port. Uses the marker header so a
56
- # foreign listener on 3001 doesn't cause us to silently skip starting.
58
+ dashboard_health_headers() {
59
+ command -v curl >/dev/null 2>&1 || return 1
60
+ curl -sfI --connect-timeout 2 --max-time 5 "http://127.0.0.1:$PORT/api/health" 2>/dev/null
61
+ }
62
+
63
+ header_value_from() {
64
+ header_name="$1"
65
+ awk -v wanted="$header_name" '
66
+ BEGIN { wanted = tolower(wanted) ":" }
67
+ {
68
+ line = $0
69
+ sub(/\r$/, "", line)
70
+ lower = tolower(line)
71
+ if (index(lower, wanted) == 1) {
72
+ sub(/^[^:]*:[[:space:]]*/, "", line)
73
+ print line
74
+ exit
75
+ }
76
+ }
77
+ '
78
+ }
79
+
80
+ canonical_dir() {
81
+ dir="$1"
82
+ (cd "$dir" 2>/dev/null && pwd -P) || printf '%s\n' "$dir"
83
+ }
84
+
85
+ normalize_identity_path() {
86
+ path="$1"
87
+ if claude_smart_is_windows; then
88
+ if command -v cygpath >/dev/null 2>&1; then
89
+ path="$(cygpath -u "$path" 2>/dev/null || printf '%s\n' "$path")"
90
+ else
91
+ path="$(
92
+ printf '%s\n' "$path" | awk '{
93
+ gsub(/\\/, "/")
94
+ if ($0 ~ /^[A-Za-z]:/) {
95
+ drive = tolower(substr($0, 1, 1))
96
+ sub(/^[A-Za-z]:/, "/" drive)
97
+ }
98
+ print
99
+ }'
100
+ )"
101
+ fi
102
+ fi
103
+ while [ "${path%/}" != "$path" ] && [ "$path" != "/" ]; do
104
+ path="${path%/}"
105
+ done
106
+ printf '%s\n' "$path"
107
+ }
108
+
109
+ # True only if *this plugin root's* dashboard is on the port. The generic
110
+ # x-claude-smart-dashboard marker is intentionally not enough: after an
111
+ # install/update, an older cache can keep serving port 3001 until restarted.
112
+ dashboard_matches_current_root() {
113
+ expected_root="$(normalize_identity_path "$(canonical_dir "$PLUGIN_ROOT")")"
114
+ headers="$(dashboard_health_headers)" || return 1
115
+ printf '%s\n' "$headers" | grep -qi '^x-claude-smart-dashboard:' || return 1
116
+ actual_root="$(printf '%s\n' "$headers" | header_value_from "x-claude-smart-plugin-root")"
117
+ [ -n "$actual_root" ] || return 1
118
+ actual_root="$(normalize_identity_path "$actual_root")"
119
+ [ "$actual_root" = "$expected_root" ]
120
+ }
121
+
122
+ # Kill a claude-smart dashboard listener currently holding the port. Gated by
123
+ # marker_responds so a foreign app on 3001 is never killed.
124
+ stop_dashboard_listener() {
125
+ marker_responds || return 0
126
+ command -v lsof >/dev/null 2>&1 || return 0
127
+ port_pid=$(lsof -t -i ":$PORT" -sTCP:LISTEN 2>/dev/null | head -n1)
128
+ [ -n "$port_pid" ] || return 0
129
+ kill -TERM "$port_pid" 2>/dev/null || true
130
+ for _ in 1 2 3 4 5; do
131
+ kill -0 "$port_pid" 2>/dev/null || return 0
132
+ sleep 0.2
133
+ done
134
+ kill -KILL "$port_pid" 2>/dev/null || true
135
+ }
136
+
137
+ # True only if *our* dashboard is on the port. Uses plugin-root identity so a
138
+ # stale claude-smart listener doesn't cause us to silently skip starting.
57
139
  is_our_dashboard_running() {
58
140
  if [ -f "$PID_FILE" ]; then
59
141
  pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
@@ -62,7 +144,7 @@ is_our_dashboard_running() {
62
144
  # don't claim "running" when the server crashed but the group leader
63
145
  # lingered.
64
146
  if command -v curl >/dev/null 2>&1; then
65
- marker_responds && return 0
147
+ dashboard_matches_current_root && return 0
66
148
  else
67
149
  # No curl — fall back to PID liveness alone.
68
150
  return 0
@@ -71,7 +153,7 @@ is_our_dashboard_running() {
71
153
  fi
72
154
  # No PID or dead PID — probe the port for our marker (recovers after a
73
155
  # stale PID file from a crash).
74
- marker_responds && return 0
156
+ dashboard_matches_current_root && return 0
75
157
  return 1
76
158
  }
77
159
 
@@ -97,6 +179,10 @@ case "$CMD" in
97
179
  fi
98
180
  if [ ! -d "$DASHBOARD_DIR" ]; then emit_ok; exit 0; fi
99
181
  if is_our_dashboard_running; then claude_smart_clear_dashboard_unavailable; emit_ok; exit 0; fi
182
+ if marker_responds; then
183
+ echo "[claude-smart] dashboard: stale claude-smart dashboard on port $PORT; restarting from $PLUGIN_ROOT" >>"$LOG_FILE"
184
+ stop_dashboard_listener
185
+ fi
100
186
  if port_occupied; then
101
187
  echo "[claude-smart] dashboard: port $PORT held by another process; skipping" >>"$LOG_FILE"
102
188
  emit_ok; exit 0
@@ -152,7 +238,7 @@ case "$CMD" in
152
238
  echo "$dash_pid" > "$PID_FILE"
153
239
  dashboard_ready=0
154
240
  for _ in 1 2 3 4 5; do
155
- if marker_responds; then
241
+ if dashboard_matches_current_root; then
156
242
  dashboard_ready=1
157
243
  claude_smart_clear_dashboard_unavailable
158
244
  break
@@ -169,21 +255,11 @@ case "$CMD" in
169
255
  kill_group "$(cat "$PID_FILE" 2>/dev/null)"
170
256
  rm -f "$PID_FILE"
171
257
  fi
172
- # Fallback: if our dashboard is still responding on the port (e.g.,
258
+ # Fallback: if a claude-smart dashboard is still responding on the port (e.g.,
173
259
  # was started outside this script, or the PGID kill missed because
174
260
  # the process wasn't the group leader) kill whoever owns the port.
175
261
  # Gated on the marker header so we never touch a foreign listener.
176
- if marker_responds && command -v lsof >/dev/null 2>&1; then
177
- port_pid=$(lsof -t -i ":$PORT" -sTCP:LISTEN 2>/dev/null | head -n1)
178
- if [ -n "$port_pid" ]; then
179
- kill -TERM "$port_pid" 2>/dev/null || true
180
- for _ in 1 2 3 4 5; do
181
- kill -0 "$port_pid" 2>/dev/null || break
182
- sleep 0.2
183
- done
184
- kill -KILL "$port_pid" 2>/dev/null || true
185
- fi
186
- fi
262
+ stop_dashboard_listener
187
263
  emit_ok
188
264
  ;;
189
265
  session-end)
@@ -18,16 +18,21 @@ case "$EVENT" in
18
18
  EVENT="${2:-}"
19
19
  ;;
20
20
  esac
21
- if [ -z "$EVENT" ]; then
22
- echo '{"continue":true,"suppressOutput":true}'
23
- exit 0
21
+ export CLAUDE_SMART_HOST="$HOST"
22
+ if [ "$HOST" = "codex" ] && [ -z "${CLAUDE_SMART_CITATION_LINK_STYLE:-}" ]; then
23
+ export CLAUDE_SMART_CITATION_LINK_STYLE="osc8"
24
24
  fi
25
25
 
26
26
  HERE="$(cd "$(dirname "$0")" && pwd)"
27
27
  # shellcheck source=_lib.sh
28
28
  . "$HERE/_lib.sh"
29
+ if [ -z "$EVENT" ]; then
30
+ claude_smart_emit_continue
31
+ exit 0
32
+ fi
33
+
29
34
  if claude_smart_is_internal_invocation_env; then
30
- echo '{"continue":true,"suppressOutput":true}'
35
+ claude_smart_emit_continue
31
36
  exit 0
32
37
  fi
33
38
  # Pick up uv from the user's login-shell PATH (covers ~/.local/bin populated
@@ -37,6 +42,7 @@ claude_smart_source_login_path
37
42
  # Explicit fallback for the astral.sh installer's default paths, in case
38
43
  # the user's login-shell rc hasn't yet been re-sourced to pick them up.
39
44
  claude_smart_prepend_astral_bins
45
+ claude_smart_source_reflexio_env
40
46
 
41
47
  PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
42
48
 
@@ -70,7 +76,7 @@ print(json.dumps({
70
76
  }))
71
77
  PY
72
78
  else
73
- echo '{"continue":true,"suppressOutput":true}'
79
+ claude_smart_emit_continue
74
80
  fi
75
81
  exit 0
76
82
  fi
@@ -82,7 +88,7 @@ if ! command -v uv >/dev/null 2>&1; then
82
88
  # the same installer detached so normal work is not blocked by first-run
83
89
  # dependency setup.
84
90
  if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then
85
- echo '{"continue":true,"suppressOutput":true}'
91
+ claude_smart_emit_continue
86
92
  exit 0
87
93
  fi
88
94
  if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
@@ -102,7 +108,7 @@ if ! command -v uv >/dev/null 2>&1; then
102
108
  fi
103
109
  fi
104
110
  if ! command -v uv >/dev/null 2>&1; then
105
- echo '{"continue":true,"suppressOutput":true}'
111
+ claude_smart_emit_continue
106
112
  exit 0
107
113
  fi
108
114
  fi
@@ -14,6 +14,7 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
14
14
  claude_smart_source_login_path
15
15
  claude_smart_prepend_astral_bins
16
16
  claude_smart_prepend_node_bins
17
+ claude_smart_source_reflexio_env
17
18
 
18
19
  PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
19
20
  REPO_ROOT="$(cd "$HERE/../.." && pwd)"
@@ -47,7 +48,7 @@ acquire_install_lock() {
47
48
  exec 9>"$INSTALL_LOCK"
48
49
  if ! flock 9; then
49
50
  echo "[claude-smart] install lock failed; continuing without serialization" >&2
50
- echo '{"continue":true,"suppressOutput":true}'
51
+ claude_smart_emit_continue
51
52
  exit 0
52
53
  fi
53
54
  return 0
@@ -91,7 +92,7 @@ write_failure() {
91
92
  } > "$FAILURE_MARKER"
92
93
  rm -f "$SUCCESS_MARKER"
93
94
  echo "[claude-smart] install failed: $reason" >&2
94
- echo '{"continue":true,"suppressOutput":true}'
95
+ claude_smart_emit_continue
95
96
  exit 0
96
97
  }
97
98
 
@@ -105,8 +106,10 @@ install_complete() {
105
106
  command -v uv >/dev/null 2>&1 || return 1
106
107
  [ -d "$PLUGIN_ROOT/.venv" ] || return 1
107
108
  [ -f "$HOME/.reflexio/.env" ] || return 1
108
- grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.reflexio/.env" || return 1
109
- grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.reflexio/.env" || return 1
109
+ if [ -z "${REFLEXIO_API_KEY:-}" ]; then
110
+ grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.reflexio/.env" || return 1
111
+ grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.reflexio/.env" || return 1
112
+ fi
110
113
  if [ -d "$PLUGIN_ROOT/dashboard" ]; then
111
114
  [ -d "$PLUGIN_ROOT/dashboard/.next" ] || [ -f "$MARKER_DIR/dashboard-build.pid" ] || [ -f "$(claude_smart_dashboard_unavailable_marker)" ] || return 1
112
115
  fi
@@ -360,7 +363,7 @@ fi
360
363
  preflight_supported_runtime_platform
361
364
 
362
365
  if install_complete; then
363
- echo '{"continue":true,"suppressOutput":true}'
366
+ claude_smart_emit_continue
364
367
  exit 0
365
368
  fi
366
369
 
@@ -431,13 +434,15 @@ fi
431
434
  REFLEXIO_ENV="$HOME/.reflexio/.env"
432
435
  mkdir -p "$(dirname "$REFLEXIO_ENV")"
433
436
  touch "$REFLEXIO_ENV"
434
- if ! grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
435
- printf '\n# Route reflexio generation through the local Claude Code CLI\nCLAUDE_SMART_USE_LOCAL_CLI=1\n' >> "$REFLEXIO_ENV"
436
- echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
437
- fi
438
- if ! grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
439
- printf '# Use the in-process ONNX embedder (chromadb) — no API key for semantic search\nCLAUDE_SMART_USE_LOCAL_EMBEDDING=1\n' >> "$REFLEXIO_ENV"
440
- echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
437
+ if [ -z "${REFLEXIO_API_KEY:-}" ]; then
438
+ if ! grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
439
+ printf '\n# Route reflexio generation through the local Claude Code CLI\nCLAUDE_SMART_USE_LOCAL_CLI=1\n' >> "$REFLEXIO_ENV"
440
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
441
+ fi
442
+ if ! grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
443
+ printf '# Use the in-process ONNX embedder (chromadb) — no API key for semantic search\nCLAUDE_SMART_USE_LOCAL_EMBEDDING=1\n' >> "$REFLEXIO_ENV"
444
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
445
+ fi
441
446
  fi
442
447
  # Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
443
448
  # plugin backend port (8071). Matches the quoted and unquoted forms but
@@ -511,4 +516,4 @@ fi
511
516
 
512
517
  write_success_marker
513
518
  echo "[claude-smart] install complete. Backend and dashboard auto-start on session start." >&2
514
- echo '{"continue":true,"suppressOutput":true}'
519
+ claude_smart_emit_continue