claude-smart 0.2.30 → 0.2.32

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 (64) hide show
  1. package/README.md +2 -2
  2. package/bin/claude-smart.js +172 -18
  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 +2 -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 +20 -1
  40. package/plugin/dashboard/package-lock.json +70 -95
  41. package/plugin/dashboard/package.json +5 -2
  42. package/plugin/hooks/hooks.json +1 -1
  43. package/plugin/pyproject.toml +1 -1
  44. package/plugin/scripts/_lib.sh +126 -0
  45. package/plugin/scripts/backend-service.sh +32 -7
  46. package/plugin/scripts/cli.sh +4 -2
  47. package/plugin/scripts/codex-hook.js +100 -3
  48. package/plugin/scripts/dashboard-service.sh +98 -19
  49. package/plugin/scripts/hook_entry.sh +32 -11
  50. package/plugin/scripts/smart-install.sh +27 -44
  51. package/plugin/src/claude_smart/cli.py +204 -20
  52. package/plugin/src/claude_smart/context_format.py +244 -6
  53. package/plugin/src/claude_smart/context_inject.py +8 -1
  54. package/plugin/src/claude_smart/cs_cite.py +186 -34
  55. package/plugin/src/claude_smart/env_config.py +102 -0
  56. package/plugin/src/claude_smart/events/session_end.py +171 -6
  57. package/plugin/src/claude_smart/events/session_start.py +26 -2
  58. package/plugin/src/claude_smart/events/stop.py +48 -9
  59. package/plugin/src/claude_smart/hook.py +62 -4
  60. package/plugin/src/claude_smart/hook_log.py +301 -0
  61. package/plugin/src/claude_smart/internal_call.py +30 -0
  62. package/plugin/src/claude_smart/publish.py +5 -0
  63. package/plugin/src/claude_smart/reflexio_adapter.py +102 -26
  64. package/plugin/uv.lock +1 -1
@@ -32,6 +32,77 @@ 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|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
+
88
+ claude_smart_is_internal_invocation_env() {
89
+ if [ "${CLAUDE_SMART_INTERNAL:-}" = "1" ]; then
90
+ return 0
91
+ fi
92
+ if [ -n "${CLAUDE_CODE_ENTRYPOINT:-}" ] && [ "${CLAUDE_CODE_ENTRYPOINT:-}" != "cli" ]; then
93
+ return 0
94
+ fi
95
+ return 1
96
+ }
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
+
35
106
  claude_smart_dashboard_unavailable_marker() {
36
107
  printf '%s\n' "$HOME/.claude-smart/dashboard-unavailable"
37
108
  }
@@ -196,6 +267,61 @@ PY
196
267
  return 1
197
268
  }
198
269
 
270
+ # Print a short fingerprint line for a file ("<cksum>:<size>" or "missing").
271
+ claude_smart_fingerprint_file() {
272
+ local path
273
+ path="$1"
274
+ if [ -f "$path" ]; then
275
+ cksum "$path" 2>/dev/null | awk '{print $1 ":" $2}'
276
+ else
277
+ printf 'missing\n'
278
+ fi
279
+ }
280
+
281
+ # Print a multi-line fingerprint covering every input that determines
282
+ # whether smart-install.sh can succeed. Order is stable so the hash is
283
+ # reproducible. Args: $1 = plugin_root, $2 = scripts_dir.
284
+ claude_smart_install_fingerprint() {
285
+ local plugin_root script_dir
286
+ plugin_root="$1"
287
+ script_dir="$2"
288
+ printf 'plugin_root=%s\n' "$plugin_root"
289
+ printf 'smart_install=%s\n' "$(claude_smart_fingerprint_file "$script_dir/smart-install.sh")"
290
+ printf 'lib=%s\n' "$(claude_smart_fingerprint_file "$script_dir/_lib.sh")"
291
+ printf 'pyproject=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/pyproject.toml")"
292
+ printf 'uv_lock=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/uv.lock")"
293
+ # Resolved python interpreter — catches a system upgrade (3.12.4 → 3.12.5)
294
+ # that would otherwise let install_complete return true against a venv
295
+ # built against a now-deleted interpreter.
296
+ if command -v uv >/dev/null 2>&1; then
297
+ printf 'python=%s\n' "$(uv python find 3.12 2>/dev/null || echo missing)"
298
+ else
299
+ printf 'python=no-uv\n'
300
+ fi
301
+ if [ -d "$plugin_root/dashboard" ]; then
302
+ printf 'dashboard_pkg=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/dashboard/package.json")"
303
+ printf 'dashboard_lock=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/dashboard/package-lock.json")"
304
+ else
305
+ printf 'dashboard_pkg=none\n'
306
+ printf 'dashboard_lock=none\n'
307
+ fi
308
+ }
309
+
310
+ # Hash the install fingerprint to a single short hex string. Used by the
311
+ # install-failed marker so hook_entry.sh can detect when inputs change and
312
+ # auto-clear a stale marker. Args: $1 = plugin_root, $2 = scripts_dir.
313
+ claude_smart_install_fingerprint_hash() {
314
+ local plugin_root script_dir tmp rc
315
+ plugin_root="$1"
316
+ script_dir="$2"
317
+ tmp=$(mktemp 2>/dev/null) || return 1
318
+ claude_smart_install_fingerprint "$plugin_root" "$script_dir" > "$tmp"
319
+ claude_smart_sha256_file "$tmp"
320
+ rc=$?
321
+ rm -f "$tmp"
322
+ return $rc
323
+ }
324
+
199
325
  # Return 0 if `node` exists and satisfies the minimum major/minor pair.
200
326
  # Patch versions are intentionally ignored because our requirement is a
201
327
  # floor, not an exact runtime pin.
@@ -22,18 +22,25 @@ 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
29
+ EMBEDDING_PORT="${EMBEDDING_PORT:-8072}"
28
30
  # Pass through to `reflexio services start/stop` so the spawned backend
29
31
  # binds to PORT instead of reflexio's library default (8081).
30
32
  export BACKEND_PORT="$PORT"
33
+ export EMBEDDING_PORT
31
34
 
32
35
  # Default: route extraction through the active host CLI + ONNX embedder
33
36
  # so claude-smart works without any LLM API key. Users can opt out by
34
37
  # pre-exporting these to 0.
35
38
  export CLAUDE_SMART_USE_LOCAL_CLI="${CLAUDE_SMART_USE_LOCAL_CLI:-1}"
36
39
  export CLAUDE_SMART_USE_LOCAL_EMBEDDING="${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}"
40
+ if [ "${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-}" = "1" ]; then
41
+ export REFLEXIO_EMBEDDING_PROVIDER="${REFLEXIO_EMBEDDING_PROVIDER:-local_service}"
42
+ export REFLEXIO_EMBEDDING_SERVICE_URL="${REFLEXIO_EMBEDDING_SERVICE_URL:-http://127.0.0.1:$EMBEDDING_PORT}"
43
+ fi
37
44
  # The backend can be spawned from contexts whose PATH lacks the host
38
45
  # CLI dir (commonly ~/.local/bin or /opt/homebrew/bin). Pin the CLI
39
46
  # explicitly if we can resolve it from our own (post-login-path) PATH.
@@ -61,7 +68,7 @@ LOG_MAX_BYTES="$(claude_smart_log_max_bytes)"
61
68
  mkdir -p "$STATE_DIR"
62
69
  claude_smart_trim_log_file "$LOG_FILE" "$LOG_MAX_BYTES"
63
70
 
64
- emit_ok() { echo '{"continue":true,"suppressOutput":true}'; }
71
+ emit_ok() { claude_smart_emit_continue; }
65
72
 
66
73
  emit_start_failure() {
67
74
  reason="$1"
@@ -173,6 +180,13 @@ full_stop() {
173
180
 
174
181
  case "$CMD" in
175
182
  start)
183
+ if claude_smart_is_internal_invocation_env; then
184
+ emit_ok; exit 0
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
176
190
  # Opt-out: users who don't want the backend managed by the hook can
177
191
  # set CLAUDE_SMART_BACKEND_AUTOSTART=0.
178
192
  if [ "${CLAUDE_SMART_BACKEND_AUTOSTART:-1}" = "0" ]; then
@@ -206,16 +220,21 @@ case "$CMD" in
206
220
  export INTERACTION_CLEANUP_THRESHOLD="${INTERACTION_CLEANUP_THRESHOLD:-500}"
207
221
  export INTERACTION_CLEANUP_DELETE_COUNT="${INTERACTION_CLEANUP_DELETE_COUNT:-200}"
208
222
 
209
- # --no-reload: uvicorn's reloader forks a supervisor; makes
210
- # bookkeeping harder and we don't need hot-reload for a user-facing
211
- # service. Detach via claude_smart_spawn_detached so the same code
212
- # path covers Linux (setsid), macOS (python3 os.setsid), and Windows
213
223
  # (nohup; no process groups). backend-log-runner.sh owns stdout/stderr
214
224
  # capture so process output cannot grow backend.log past its cap.
225
+ #
226
+ # --workers: reflexio defaults to 2 (zero-downtime worker recycling
227
+ # for server deployments). For a single-user Claude Code plugin
228
+ # that's pure overhead: ~1.1 GB extra RSS, periodic 5–10 s spawn
229
+ # hiccups during worker rotation, and SQLite can't accept concurrent
230
+ # writers anyway. Default to 1 here; opt in to N via
231
+ # CLAUDE_SMART_BACKEND_WORKERS for power users running concurrent
232
+ # Claude Code sessions or wanting zero-downtime recycling.
233
+ workers="${CLAUDE_SMART_BACKEND_WORKERS:-1}"
215
234
  claude_smart_spawn_detached bash "$HERE/backend-log-runner.sh" \
216
235
  "$LOG_FILE" "$LOG_MAX_BYTES" -- \
217
236
  uv run --project "$PLUGIN_ROOT" --quiet \
218
- reflexio services start --only backend --no-reload
237
+ reflexio services start --only backend --no-reload --workers "$workers"
219
238
  svc_pid=$!
220
239
  # Record the spawned pid, not a pgid sampled with ps. On POSIX,
221
240
  # setsid/python os.setsid make this pid the new process group leader;
@@ -255,7 +274,13 @@ case "$CMD" in
255
274
  emit_ok
256
275
  ;;
257
276
  status)
258
- 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
259
284
  ;;
260
285
  *)
261
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
 
@@ -24,7 +25,7 @@ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
24
25
  # on a broken install.
25
26
  FAILURE_MARKER="$HOME/.claude-smart/install-failed"
26
27
  if [ -f "$FAILURE_MARKER" ]; then
27
- msg="$(cat "$FAILURE_MARKER" 2>/dev/null || echo "")"
28
+ msg="$(head -n 1 "$FAILURE_MARKER" 2>/dev/null || echo "")"
28
29
  [ -n "$msg" ] || msg="unknown error"
29
30
  echo "claude-smart is not installed correctly: $msg" >&2
30
31
  echo "Re-run the plugin's Setup (restart Claude Code) or fix the underlying issue and delete $FAILURE_MARKER to retry." >&2
@@ -50,7 +51,8 @@ if ! command -v uv >/dev/null 2>&1; then
50
51
  fi
51
52
  if ! command -v uv >/dev/null 2>&1; then
52
53
  if [ -f "$FAILURE_MARKER" ]; then
53
- msg="$(cat "$FAILURE_MARKER" 2>/dev/null || echo "unknown error")"
54
+ msg="$(head -n 1 "$FAILURE_MARKER" 2>/dev/null || echo "")"
55
+ [ -n "$msg" ] || msg="unknown error"
54
56
  echo "claude-smart: install failed: $msg" >&2
55
57
  echo "Fix the underlying issue and delete $FAILURE_MARKER to retry." >&2
56
58
  else
@@ -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,56 @@ 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
+ "CLAUDE_SMART_USE_LOCAL_CLI",
187
+ "CLAUDE_SMART_USE_LOCAL_EMBEDDING",
188
+ "CLAUDE_SMART_BACKEND_AUTOSTART",
189
+ "CLAUDE_SMART_DASHBOARD_AUTOSTART",
190
+ "CLAUDE_SMART_CLI_PATH",
191
+ "CLAUDE_SMART_CLI_TIMEOUT",
192
+ "CLAUDE_SMART_STATE_DIR",
193
+ "CLAUDE_SMART_ENABLE_OPTIMIZER",
194
+ ]);
195
+ for (const rawLine of text.split(/\r?\n/)) {
196
+ let line = rawLine.trim();
197
+ if (!line || line.startsWith("#")) continue;
198
+ if (line.startsWith("export ")) line = line.slice("export ".length).trimStart();
199
+ const eq = line.indexOf("=");
200
+ if (eq < 0) continue;
201
+ const key = line.slice(0, eq).trim();
202
+ if (!allowed.has(key) || process.env[key]) continue;
203
+ process.env[key] = unquoteEnvValue(line.slice(eq + 1));
204
+ }
205
+ }
206
+
207
+ function reflexioUrlIsRemote() {
208
+ const url = process.env.REFLEXIO_URL || "";
209
+ if (!url) return false;
210
+ return !/^http:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?\/?$/i.test(url);
211
+ }
212
+
124
213
  function codexCompatPath(root) {
125
214
  const filename = process.platform === "win32"
126
215
  ? "codex-claude-compat.cmd"
@@ -294,6 +383,11 @@ async function startBackend(root) {
294
383
  emitOk();
295
384
  return;
296
385
  }
386
+ if (reflexioUrlIsRemote()) {
387
+ appendLog("backend.log", "[claude-smart] backend: remote REFLEXIO_URL configured; skipping local backend start");
388
+ emitOk();
389
+ return;
390
+ }
297
391
  const pidFile = path.join(STATE_DIR, "backend.pid");
298
392
  for (const port of [DEFAULT_BACKEND_PORT, FALLBACK_BACKEND_PORT]) {
299
393
  if (pidAlive(readPid(pidFile)) && await healthOk(port, "/health")) {
@@ -419,7 +513,7 @@ function runHook(root, event) {
419
513
  }
420
514
  if (!uv) {
421
515
  appendLog("backend.log", "[claude-smart] hook: uv not on PATH after installer; skipping");
422
- emitOk();
516
+ emitHookOk();
423
517
  return 0;
424
518
  }
425
519
  }
@@ -434,17 +528,20 @@ function runHook(root, event) {
434
528
  REFLEXIO_URL: readBackendUrl(),
435
529
  CLAUDE_SMART_HOST: "codex",
436
530
  CLAUDE_SMART_CLI_PATH: process.env.CLAUDE_SMART_CLI_PATH || codexCompatPath(root),
531
+ CLAUDE_SMART_CITATION_LINK_STYLE: process.env.CLAUDE_SMART_CITATION_LINK_STYLE || "osc8",
437
532
  },
438
533
  input,
439
- stdio: ["pipe", "inherit", "inherit"],
534
+ stdio: ["pipe", "pipe", "inherit"],
440
535
  windowsHide: true,
441
536
  },
442
537
  );
538
+ emitNormalizedHookOutput(result.stdout);
443
539
  return typeof result.status === "number" ? result.status : 1;
444
540
  }
445
541
 
446
542
  async function main() {
447
543
  prependRuntimePath();
544
+ loadReflexioEnv();
448
545
  const root = pluginRoot();
449
546
  process.env.PLUGIN_ROOT = root;
450
547
  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
 
@@ -87,6 +169,9 @@ port_occupied() {
87
169
 
88
170
  case "$CMD" in
89
171
  start)
172
+ if claude_smart_is_internal_invocation_env; then
173
+ emit_ok; exit 0
174
+ fi
90
175
  # Opt-out: users who don't want the dashboard long-lived can set
91
176
  # CLAUDE_SMART_DASHBOARD_AUTOSTART=0 in their environment.
92
177
  if [ "${CLAUDE_SMART_DASHBOARD_AUTOSTART:-1}" = "0" ]; then
@@ -94,6 +179,10 @@ case "$CMD" in
94
179
  fi
95
180
  if [ ! -d "$DASHBOARD_DIR" ]; then emit_ok; exit 0; fi
96
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
97
186
  if port_occupied; then
98
187
  echo "[claude-smart] dashboard: port $PORT held by another process; skipping" >>"$LOG_FILE"
99
188
  emit_ok; exit 0
@@ -149,7 +238,7 @@ case "$CMD" in
149
238
  echo "$dash_pid" > "$PID_FILE"
150
239
  dashboard_ready=0
151
240
  for _ in 1 2 3 4 5; do
152
- if marker_responds; then
241
+ if dashboard_matches_current_root; then
153
242
  dashboard_ready=1
154
243
  claude_smart_clear_dashboard_unavailable
155
244
  break
@@ -166,21 +255,11 @@ case "$CMD" in
166
255
  kill_group "$(cat "$PID_FILE" 2>/dev/null)"
167
256
  rm -f "$PID_FILE"
168
257
  fi
169
- # 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.,
170
259
  # was started outside this script, or the PGID kill missed because
171
260
  # the process wasn't the group leader) kill whoever owns the port.
172
261
  # Gated on the marker header so we never touch a foreign listener.
173
- if marker_responds && command -v lsof >/dev/null 2>&1; then
174
- port_pid=$(lsof -t -i ":$PORT" -sTCP:LISTEN 2>/dev/null | head -n1)
175
- if [ -n "$port_pid" ]; then
176
- kill -TERM "$port_pid" 2>/dev/null || true
177
- for _ in 1 2 3 4 5; do
178
- kill -0 "$port_pid" 2>/dev/null || break
179
- sleep 0.2
180
- done
181
- kill -KILL "$port_pid" 2>/dev/null || true
182
- fi
183
- fi
262
+ stop_dashboard_listener
184
263
  emit_ok
185
264
  ;;
186
265
  session-end)
@@ -18,14 +18,23 @@ 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
+
34
+ if claude_smart_is_internal_invocation_env; then
35
+ claude_smart_emit_continue
36
+ exit 0
37
+ fi
29
38
  # Pick up uv from the user's login-shell PATH (covers ~/.local/bin populated
30
39
  # by the astral.sh installer) so a fresh install works before the user
31
40
  # restarts their terminal. Matches the pattern used by smart-install.sh.
@@ -33,16 +42,27 @@ claude_smart_source_login_path
33
42
  # Explicit fallback for the astral.sh installer's default paths, in case
34
43
  # the user's login-shell rc hasn't yet been re-sourced to pick them up.
35
44
  claude_smart_prepend_astral_bins
45
+ claude_smart_source_reflexio_env
36
46
 
37
47
  PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
38
48
 
39
49
  FAILURE_MARKER="$HOME/.claude-smart/install-failed"
40
50
  STATE_DIR="$HOME/.claude-smart"
41
51
  if [ -f "$FAILURE_MARKER" ]; then
42
- if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then
43
- python3 - "$FAILURE_MARKER" <<'PY'
52
+ # Auto-clear the marker when install inputs have changed since the
53
+ # failure was recorded (pyproject.toml / uv.lock / smart-install.sh /
54
+ # python interpreter etc.). Without this the plugin stays muted forever
55
+ # after a transient sync failure even when the user has fixed it.
56
+ stored_fp="$(awk -F= '$1=="fingerprint"{print $2; exit}' "$FAILURE_MARKER" 2>/dev/null || true)"
57
+ current_fp="$(claude_smart_install_fingerprint_hash "$PLUGIN_ROOT" "$HERE" 2>/dev/null || true)"
58
+ if [ -z "$stored_fp" ] || [ "$stored_fp" != "$current_fp" ]; then
59
+ rm -f "$FAILURE_MARKER"
60
+ else
61
+ if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then
62
+ python3 - "$FAILURE_MARKER" <<'PY'
44
63
  import json, pathlib, sys
45
- msg = pathlib.Path(sys.argv[1]).read_text().strip() or "unknown error"
64
+ first = pathlib.Path(sys.argv[1]).read_text().splitlines()
65
+ msg = (first[0].strip() if first else "") or "unknown error"
46
66
  print(json.dumps({
47
67
  "hookSpecificOutput": {
48
68
  "hookEventName": "SessionStart",
@@ -55,10 +75,11 @@ print(json.dumps({
55
75
  }
56
76
  }))
57
77
  PY
58
- else
59
- echo '{"continue":true,"suppressOutput":true}'
78
+ else
79
+ claude_smart_emit_continue
80
+ fi
81
+ exit 0
60
82
  fi
61
- exit 0
62
83
  fi
63
84
 
64
85
  if ! command -v uv >/dev/null 2>&1; then
@@ -67,7 +88,7 @@ if ! command -v uv >/dev/null 2>&1; then
67
88
  # the same installer detached so normal work is not blocked by first-run
68
89
  # dependency setup.
69
90
  if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then
70
- echo '{"continue":true,"suppressOutput":true}'
91
+ claude_smart_emit_continue
71
92
  exit 0
72
93
  fi
73
94
  if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
@@ -87,7 +108,7 @@ if ! command -v uv >/dev/null 2>&1; then
87
108
  fi
88
109
  fi
89
110
  if ! command -v uv >/dev/null 2>&1; then
90
- echo '{"continue":true,"suppressOutput":true}'
111
+ claude_smart_emit_continue
91
112
  exit 0
92
113
  fi
93
114
  fi