claude-smart 0.2.29 → 0.2.31

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.
package/README.md CHANGED
@@ -6,14 +6,14 @@
6
6
  claude-smart
7
7
  </h1>
8
8
 
9
- <h4 align="center">A local learning plugin for <a href="https://claude.com/claude-code" target="_blank">Claude Code</a> and Codex that turns corrections into durable rules your coding assistant follows in future sessions.</h4>
9
+ <h4 align="center">A self-improvement plugin for <a href="https://claude.com/claude-code" target="_blank">Claude Code</a> and Codex that turns interactions into durable skills they follow in future sessions.</h4>
10
10
 
11
11
  <p align="center">
12
12
  <a href="LICENSE">
13
13
  <img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License">
14
14
  </a>
15
15
  <a href="plugin/pyproject.toml">
16
- <img src="https://img.shields.io/badge/version-0.2.29-green.svg" alt="Version">
16
+ <img src="https://img.shields.io/badge/version-0.2.31-green.svg" alt="Version">
17
17
  </a>
18
18
  <a href="plugin/pyproject.toml">
19
19
  <img src="https://img.shields.io/badge/python-%3E%3D3.12-brightgreen.svg" alt="Python">
@@ -24,9 +24,6 @@
24
24
  <a href="#quick-start">
25
25
  <img src="https://img.shields.io/badge/hosts-Claude%20Code%20%2B%20Codex-purple.svg" alt="Hosts">
26
26
  </a>
27
- <a href="https://discord.gg/Jbft3jPn">
28
- <img src="https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white" alt="Discord">
29
- </a>
30
27
  </p>
31
28
 
32
29
  <p align="center">
@@ -40,22 +37,22 @@
40
37
  </p>
41
38
 
42
39
  <p align="center">
43
- It learns both corrections and successful execution patterns—so your coding assistant avoids repeating mistakes and reuses what works. Project-specific skills capture repo-local rules, and shared skills roll up durable patterns for reuse across projects.
40
+ claude-smart learns from your corrections <i>and</i> from what's already working so the assistant stops repeating mistakes and reuses proven paths. Project-specific skills capture repo-local rules, and shared skills roll up durable patterns for reuse across projects.
44
41
  </p>
45
42
 
46
43
  <p align="center">
47
- <b>vs <code>claude-mem</code>:</b> ~3× better at turning your corrections into rules Claude follows, ~50% more of what you tell Claude sticks. <a href="EXPERIMENT.md"><b>Read the benchmark →</b></a>
44
+ <b>vs <code>claude-mem</code>:</b> ~3× better at turning your corrections into rules Claude follows, ~50% more of your guidance is retained. <a href="EXPERIMENT.md"><b>Read the benchmark →</b></a>
48
45
  </p>
49
46
 
50
47
  ---
51
48
 
52
49
  ## Why Learning, Not Memory
53
50
 
54
- Most memory solutions are still mostly informative—Claude remembers what happened, without necessarily changing what it does next.
51
+ Most memory tools just record. Claude remembers what happened, but doesn't change what it does next.
55
52
 
56
53
  `claude-smart` focuses on learning instead.
57
54
 
58
- Four ways this changes what your coding assistant can do for you:
55
+ Four things this changes:
59
56
 
60
57
  - 💡 **Stop repeating the same mistakes:** Produces actionable skills Claude can follow next time; memory only records what happened.
61
58
 
@@ -63,12 +60,12 @@ Four ways this changes what your coding assistant can do for you:
63
60
  > **Memory:** “deploy broke after prisma bump; user rolled back”<br>
64
61
  > **Learning:** “treat major-version bumps of ORMs/DB drivers as breaking — verify with integration tests, not just unit tests”
65
62
 
66
- - 🚀 **Start from the optimized path:** Preserves and optimizes execution paths so Claude can reuse what already works.
63
+ - 🚀 **Start from the optimized path:** Records and optimizes the steps that worked so Claude can reuse them, instead of re-exploring each session.
67
64
  > *Example:* Claude spends several iterations trying to start the local dev environment before discovering that this repo requires `pnpm dev:all` instead of the usual `npm run dev`.<br>
68
65
  > **Memory:** “user mentioned that `npm run dev` did not work”<br>
69
66
  > **Learning:** “for this repo, always use `pnpm dev:all` to start the full local stack — `npm run dev` only starts the frontend and causes missing service errors”
70
67
 
71
- Instead of re-exploring, Claude starts from the proven path—reducing planning steps, latency, and token usage.
68
+ Instead of re-exploring, Claude Code starts from the proven path—reducing planning steps, latency, and token usage.
72
69
 
73
70
  - 🌐 **Project-specific and shared skills:** Session memory disappears with the conversation. Project-specific skills preserve repo-local rules, while shared skills roll up patterns that should transfer across projects. Preferences stay scoped to the current project so per-repo preferences don't leak across projects.
74
71
 
@@ -95,12 +92,6 @@ claude plugin marketplace add ReflexioAI/claude-smart
95
92
  claude plugin install claude-smart@reflexioai
96
93
  ```
97
94
 
98
- The plugin Setup hook installs its own `uv`, Python 3.12 environment, and
99
- private Node.js/npm runtime under `~/.claude-smart/` when they are missing, so
100
- you do not need to install Python or Node globally for the plugin/dashboard.
101
- On native Windows, Claude Code hooks still need a Git Bash-compatible `bash`
102
- until Claude Code exposes a single cross-platform hook command shape.
103
-
104
95
  To uninstall:
105
96
 
106
97
  ```bash
@@ -123,11 +114,6 @@ Then fully quit and reopen Codex so hooks reload.
123
114
 
124
115
  Requires the `codex` CLI on `PATH` and Node.js (for `npx`).
125
116
 
126
- The `npx` command needs Node.js only to launch the installer. The installed
127
- plugin uses a private Node.js/npm runtime and a `uv`-managed Python 3.12
128
- environment under `~/.claude-smart/`, so hooks and the dashboard do not depend
129
- on global Python, uv, Node, or npm after install.
130
-
131
117
  To uninstall:
132
118
 
133
119
  ```bash
@@ -170,7 +156,7 @@ the first local embedding model download. The ONNX model cache lives at
170
156
 
171
157
  ## Dashboard
172
158
 
173
- A web UI for browsing session histories, inspecting preferences, and editing project-specific and shared skills. The dashboard auto-starts alongside the backend, so you can open **http://localhost:3001** directly. Or run `/claude-smart:dashboard` in Claude Code to launch dashboard in browser. In Codex, run `bash ~/.reflexio/plugin-root/scripts/dashboard-open.sh`.
159
+ A web UI for browsing session histories, inspecting preferences, and editing project-specific and shared skills. The dashboard auto-starts alongside the backend, so you can open **http://localhost:3001** directly. Or run `/claude-smart:dashboard` in Claude Code to open it in your browser. In Codex, run `bash ~/.reflexio/plugin-root/scripts/dashboard-open.sh`.
174
160
 
175
161
  <p align="center">
176
162
  <img src="assets/preferences_dashboard.png" alt="Preferences dashboard" width="49%">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections via reflexio",
5
5
  "keywords": [
6
6
  "claude",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.29",
3
+ "version": "0.2.31",
4
4
  "description": "Self-improving coding assistant plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -18,17 +18,17 @@
18
18
  {
19
19
  "type": "command",
20
20
  "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/backend-service.sh\" start",
21
- "timeout": 30
21
+ "timeout": 300
22
22
  },
23
23
  {
24
24
  "type": "command",
25
25
  "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/dashboard-service.sh\" start",
26
- "timeout": 10
26
+ "timeout": 300
27
27
  },
28
28
  {
29
29
  "type": "command",
30
30
  "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex session-start",
31
- "timeout": 30,
31
+ "timeout": 300,
32
32
  "statusMessage": "Loading claude-smart context"
33
33
  }
34
34
  ]
@@ -35,17 +35,17 @@
35
35
  {
36
36
  "type": "command",
37
37
  "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/hook_entry.sh\" claude-code session-start",
38
- "timeout": 30
38
+ "timeout": 300
39
39
  },
40
40
  {
41
41
  "type": "command",
42
42
  "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/backend-service.sh\" start",
43
- "timeout": 30
43
+ "timeout": 300
44
44
  },
45
45
  {
46
46
  "type": "command",
47
47
  "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/dashboard-service.sh\" start",
48
- "timeout": 10
48
+ "timeout": 300
49
49
  }
50
50
  ]
51
51
  }
@@ -102,7 +102,7 @@
102
102
  {
103
103
  "type": "command",
104
104
  "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/hook_entry.sh\" claude-code session-end",
105
- "timeout": 60
105
+ "timeout": 300
106
106
  },
107
107
  {
108
108
  "type": "command",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.29"
3
+ version = "0.2.31"
4
4
  description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -32,6 +32,16 @@ claude_smart_prepend_node_bins() {
32
32
  export PATH="$_CS_NODE_ROOT/bin:$_CS_NODE_ROOT:$PATH"
33
33
  }
34
34
 
35
+ claude_smart_is_internal_invocation_env() {
36
+ if [ "${CLAUDE_SMART_INTERNAL:-}" = "1" ]; then
37
+ return 0
38
+ fi
39
+ if [ -n "${CLAUDE_CODE_ENTRYPOINT:-}" ] && [ "${CLAUDE_CODE_ENTRYPOINT:-}" != "cli" ]; then
40
+ return 0
41
+ fi
42
+ return 1
43
+ }
44
+
35
45
  claude_smart_dashboard_unavailable_marker() {
36
46
  printf '%s\n' "$HOME/.claude-smart/dashboard-unavailable"
37
47
  }
@@ -196,6 +206,61 @@ PY
196
206
  return 1
197
207
  }
198
208
 
209
+ # Print a short fingerprint line for a file ("<cksum>:<size>" or "missing").
210
+ claude_smart_fingerprint_file() {
211
+ local path
212
+ path="$1"
213
+ if [ -f "$path" ]; then
214
+ cksum "$path" 2>/dev/null | awk '{print $1 ":" $2}'
215
+ else
216
+ printf 'missing\n'
217
+ fi
218
+ }
219
+
220
+ # Print a multi-line fingerprint covering every input that determines
221
+ # whether smart-install.sh can succeed. Order is stable so the hash is
222
+ # reproducible. Args: $1 = plugin_root, $2 = scripts_dir.
223
+ claude_smart_install_fingerprint() {
224
+ local plugin_root script_dir
225
+ plugin_root="$1"
226
+ script_dir="$2"
227
+ printf 'plugin_root=%s\n' "$plugin_root"
228
+ printf 'smart_install=%s\n' "$(claude_smart_fingerprint_file "$script_dir/smart-install.sh")"
229
+ printf 'lib=%s\n' "$(claude_smart_fingerprint_file "$script_dir/_lib.sh")"
230
+ printf 'pyproject=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/pyproject.toml")"
231
+ printf 'uv_lock=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/uv.lock")"
232
+ # Resolved python interpreter — catches a system upgrade (3.12.4 → 3.12.5)
233
+ # that would otherwise let install_complete return true against a venv
234
+ # built against a now-deleted interpreter.
235
+ if command -v uv >/dev/null 2>&1; then
236
+ printf 'python=%s\n' "$(uv python find 3.12 2>/dev/null || echo missing)"
237
+ else
238
+ printf 'python=no-uv\n'
239
+ fi
240
+ if [ -d "$plugin_root/dashboard" ]; then
241
+ printf 'dashboard_pkg=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/dashboard/package.json")"
242
+ printf 'dashboard_lock=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/dashboard/package-lock.json")"
243
+ else
244
+ printf 'dashboard_pkg=none\n'
245
+ printf 'dashboard_lock=none\n'
246
+ fi
247
+ }
248
+
249
+ # Hash the install fingerprint to a single short hex string. Used by the
250
+ # install-failed marker so hook_entry.sh can detect when inputs change and
251
+ # auto-clear a stale marker. Args: $1 = plugin_root, $2 = scripts_dir.
252
+ claude_smart_install_fingerprint_hash() {
253
+ local plugin_root script_dir tmp rc
254
+ plugin_root="$1"
255
+ script_dir="$2"
256
+ tmp=$(mktemp 2>/dev/null) || return 1
257
+ claude_smart_install_fingerprint "$plugin_root" "$script_dir" > "$tmp"
258
+ claude_smart_sha256_file "$tmp"
259
+ rc=$?
260
+ rm -f "$tmp"
261
+ return $rc
262
+ }
263
+
199
264
  # Return 0 if `node` exists and satisfies the minimum major/minor pair.
200
265
  # Patch versions are intentionally ignored because our requirement is a
201
266
  # floor, not an exact runtime pin.
@@ -25,15 +25,21 @@ claude_smart_prepend_astral_bins
25
25
 
26
26
  CMD="${1:-start}"
27
27
  PORT=8071
28
+ EMBEDDING_PORT="${EMBEDDING_PORT:-8072}"
28
29
  # Pass through to `reflexio services start/stop` so the spawned backend
29
30
  # binds to PORT instead of reflexio's library default (8081).
30
31
  export BACKEND_PORT="$PORT"
32
+ export EMBEDDING_PORT
31
33
 
32
34
  # Default: route extraction through the active host CLI + ONNX embedder
33
35
  # so claude-smart works without any LLM API key. Users can opt out by
34
36
  # pre-exporting these to 0.
35
37
  export CLAUDE_SMART_USE_LOCAL_CLI="${CLAUDE_SMART_USE_LOCAL_CLI:-1}"
36
38
  export CLAUDE_SMART_USE_LOCAL_EMBEDDING="${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}"
39
+ if [ "${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-}" = "1" ]; then
40
+ export REFLEXIO_EMBEDDING_PROVIDER="${REFLEXIO_EMBEDDING_PROVIDER:-local_service}"
41
+ export REFLEXIO_EMBEDDING_SERVICE_URL="${REFLEXIO_EMBEDDING_SERVICE_URL:-http://127.0.0.1:$EMBEDDING_PORT}"
42
+ fi
37
43
  # The backend can be spawned from contexts whose PATH lacks the host
38
44
  # CLI dir (commonly ~/.local/bin or /opt/homebrew/bin). Pin the CLI
39
45
  # explicitly if we can resolve it from our own (post-login-path) PATH.
@@ -173,6 +179,9 @@ full_stop() {
173
179
 
174
180
  case "$CMD" in
175
181
  start)
182
+ if claude_smart_is_internal_invocation_env; then
183
+ emit_ok; exit 0
184
+ fi
176
185
  # Opt-out: users who don't want the backend managed by the hook can
177
186
  # set CLAUDE_SMART_BACKEND_AUTOSTART=0.
178
187
  if [ "${CLAUDE_SMART_BACKEND_AUTOSTART:-1}" = "0" ]; then
@@ -186,8 +195,16 @@ case "$CMD" in
186
195
  emit_ok; exit 0
187
196
  fi
188
197
  if ! command -v uv >/dev/null 2>&1; then
189
- claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; skipping"
190
- emit_ok; exit 0
198
+ if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
199
+ claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; running installer"
200
+ CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true
201
+ claude_smart_source_login_path
202
+ claude_smart_prepend_astral_bins
203
+ fi
204
+ if ! command -v uv >/dev/null 2>&1; then
205
+ claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH after installer; skipping"
206
+ emit_ok; exit 0
207
+ fi
191
208
  fi
192
209
  cd "$PLUGIN_ROOT"
193
210
 
@@ -198,16 +215,21 @@ case "$CMD" in
198
215
  export INTERACTION_CLEANUP_THRESHOLD="${INTERACTION_CLEANUP_THRESHOLD:-500}"
199
216
  export INTERACTION_CLEANUP_DELETE_COUNT="${INTERACTION_CLEANUP_DELETE_COUNT:-200}"
200
217
 
201
- # --no-reload: uvicorn's reloader forks a supervisor; makes
202
- # bookkeeping harder and we don't need hot-reload for a user-facing
203
- # service. Detach via claude_smart_spawn_detached so the same code
204
- # path covers Linux (setsid), macOS (python3 os.setsid), and Windows
205
218
  # (nohup; no process groups). backend-log-runner.sh owns stdout/stderr
206
219
  # capture so process output cannot grow backend.log past its cap.
220
+ #
221
+ # --workers: reflexio defaults to 2 (zero-downtime worker recycling
222
+ # for server deployments). For a single-user Claude Code plugin
223
+ # that's pure overhead: ~1.1 GB extra RSS, periodic 5–10 s spawn
224
+ # hiccups during worker rotation, and SQLite can't accept concurrent
225
+ # writers anyway. Default to 1 here; opt in to N via
226
+ # CLAUDE_SMART_BACKEND_WORKERS for power users running concurrent
227
+ # Claude Code sessions or wanting zero-downtime recycling.
228
+ workers="${CLAUDE_SMART_BACKEND_WORKERS:-1}"
207
229
  claude_smart_spawn_detached bash "$HERE/backend-log-runner.sh" \
208
230
  "$LOG_FILE" "$LOG_MAX_BYTES" -- \
209
231
  uv run --project "$PLUGIN_ROOT" --quiet \
210
- reflexio services start --only backend --no-reload
232
+ reflexio services start --only backend --no-reload --workers "$workers"
211
233
  svc_pid=$!
212
234
  # Record the spawned pid, not a pgid sampled with ps. On POSIX,
213
235
  # setsid/python os.setsid make this pid the new process group leader;
@@ -24,7 +24,7 @@ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
24
24
  # on a broken install.
25
25
  FAILURE_MARKER="$HOME/.claude-smart/install-failed"
26
26
  if [ -f "$FAILURE_MARKER" ]; then
27
- msg="$(cat "$FAILURE_MARKER" 2>/dev/null || echo "")"
27
+ msg="$(head -n 1 "$FAILURE_MARKER" 2>/dev/null || echo "")"
28
28
  [ -n "$msg" ] || msg="unknown error"
29
29
  echo "claude-smart is not installed correctly: $msg" >&2
30
30
  echo "Re-run the plugin's Setup (restart Claude Code) or fix the underlying issue and delete $FAILURE_MARKER to retry." >&2
@@ -50,7 +50,8 @@ if ! command -v uv >/dev/null 2>&1; then
50
50
  fi
51
51
  if ! command -v uv >/dev/null 2>&1; then
52
52
  if [ -f "$FAILURE_MARKER" ]; then
53
- msg="$(cat "$FAILURE_MARKER" 2>/dev/null || echo "unknown error")"
53
+ msg="$(head -n 1 "$FAILURE_MARKER" 2>/dev/null || echo "")"
54
+ [ -n "$msg" ] || msg="unknown error"
54
55
  echo "claude-smart: install failed: $msg" >&2
55
56
  echo "Fix the underlying issue and delete $FAILURE_MARKER to retry." >&2
56
57
  else
@@ -104,6 +104,10 @@ function npmPath() {
104
104
  return commandPath(process.platform === "win32" ? ["npm.cmd", "npm.exe", "npm"] : ["npm"]);
105
105
  }
106
106
 
107
+ function bashPath() {
108
+ return commandPath(process.platform === "win32" ? ["bash.exe", "bash"] : ["bash"]);
109
+ }
110
+
107
111
  function stateFile(name) {
108
112
  return path.join(STATE_DIR, name);
109
113
  }
@@ -202,6 +206,49 @@ function detached(command, args, options = {}) {
202
206
  return child.pid;
203
207
  }
204
208
 
209
+ function runInstaller(root, reason) {
210
+ if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false;
211
+ const script = path.join(root, "scripts", "smart-install.sh");
212
+ if (!fs.existsSync(script)) return false;
213
+ const bash = bashPath();
214
+ if (!bash) return false;
215
+ appendLog("backend.log", `[claude-smart] ${reason}; running installer`);
216
+ const result = spawnSync(bash, [script], {
217
+ cwd: root,
218
+ env: {
219
+ ...process.env,
220
+ CLAUDE_SMART_BOOTSTRAPPING: "1",
221
+ },
222
+ encoding: "utf8",
223
+ maxBuffer: 20 * 1024 * 1024,
224
+ windowsHide: true,
225
+ });
226
+ const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
227
+ if (output) {
228
+ ensureDir(STATE_DIR);
229
+ fs.appendFileSync(path.join(STATE_DIR, "install.log"), `${output}\n`);
230
+ trimLog(path.join(STATE_DIR, "install.log"));
231
+ }
232
+ prependRuntimePath();
233
+ return result.status === 0;
234
+ }
235
+
236
+ function startInstallerDetached(root, reason) {
237
+ if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false;
238
+ const script = path.join(root, "scripts", "smart-install.sh");
239
+ const bash = bashPath();
240
+ if (!bash || !fs.existsSync(script)) return false;
241
+ appendLog("backend.log", `[claude-smart] ${reason}; starting installer in background`);
242
+ detached(bash, [script], {
243
+ cwd: root,
244
+ env: {
245
+ ...process.env,
246
+ CLAUDE_SMART_BOOTSTRAPPING: "1",
247
+ },
248
+ });
249
+ return true;
250
+ }
251
+
205
252
  function readPid(file) {
206
253
  try {
207
254
  const value = fs.readFileSync(file, "utf8").trim();
@@ -262,7 +309,11 @@ async function startBackend(root) {
262
309
  }
263
310
  const uv = uvPath();
264
311
  if (!uv) {
265
- appendLog("backend.log", "[claude-smart] backend: uv not on PATH; skipping");
312
+ runInstaller(root, "backend: uv not on PATH");
313
+ }
314
+ const readyUv = uvPath();
315
+ if (!readyUv) {
316
+ appendLog("backend.log", "[claude-smart] backend: uv not on PATH after installer; skipping");
266
317
  emitOk();
267
318
  return;
268
319
  }
@@ -284,7 +335,7 @@ async function startBackend(root) {
284
335
  INTERACTION_CLEANUP_DELETE_COUNT: process.env.INTERACTION_CLEANUP_DELETE_COUNT || "200",
285
336
  };
286
337
  const pid = detached(
287
- uv,
338
+ readyUv,
288
339
  [
289
340
  "run",
290
341
  "--project",
@@ -326,14 +377,18 @@ async function startDashboard(root) {
326
377
  }
327
378
  const npm = npmPath();
328
379
  if (!npm) {
329
- appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH; skipping");
380
+ runInstaller(root, "dashboard: npm not on PATH");
381
+ }
382
+ const readyNpm = npmPath();
383
+ if (!readyNpm) {
384
+ appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH after installer; skipping");
330
385
  emitOk();
331
386
  return;
332
387
  }
333
388
  if (!fs.existsSync(path.join(dashboard, ".next"))) {
334
389
  const buildPidFile = path.join(STATE_DIR, "dashboard-build.pid");
335
390
  if (!pidAlive(readPid(buildPidFile))) {
336
- const pid = detached(npm, ["run", "build"], { cwd: dashboard });
391
+ const pid = detached(readyNpm, ["run", "build"], { cwd: dashboard });
337
392
  writePid(buildPidFile, pid);
338
393
  appendLog("dashboard.log", "[claude-smart] dashboard: .next missing; started background build");
339
394
  }
@@ -346,7 +401,7 @@ async function startDashboard(root) {
346
401
  REFLEXIO_URL: readBackendUrl(),
347
402
  CLAUDE_SMART_DASHBOARD_WORKSPACE: process.cwd(),
348
403
  };
349
- const pid = detached(npm, ["run", "start"], { cwd: dashboard, env });
404
+ const pid = detached(readyNpm, ["run", "start"], { cwd: dashboard, env });
350
405
  writePid(pidFile, pid);
351
406
  await waitForHealth(DASHBOARD_PORT, "/api/health", "x-claude-smart-dashboard", 5);
352
407
  emitOk();
@@ -354,11 +409,19 @@ async function startDashboard(root) {
354
409
 
355
410
  function runHook(root, event) {
356
411
  trimLog(path.join(STATE_DIR, "backend.log"));
357
- const uv = uvPath();
412
+ let uv = uvPath();
358
413
  if (!uv) {
359
- appendLog("backend.log", "[claude-smart] hook: uv not on PATH; skipping");
360
- emitOk();
361
- return 0;
414
+ if (event === "session-start") {
415
+ runInstaller(root, "hook: uv not on PATH");
416
+ uv = uvPath();
417
+ } else {
418
+ startInstallerDetached(root, "hook: uv not on PATH");
419
+ }
420
+ if (!uv) {
421
+ appendLog("backend.log", "[claude-smart] hook: uv not on PATH after installer; skipping");
422
+ emitOk();
423
+ return 0;
424
+ }
362
425
  }
363
426
  const input = fs.readFileSync(0);
364
427
  const result = spawnSync(
@@ -87,6 +87,9 @@ port_occupied() {
87
87
 
88
88
  case "$CMD" in
89
89
  start)
90
+ if claude_smart_is_internal_invocation_env; then
91
+ emit_ok; exit 0
92
+ fi
90
93
  # Opt-out: users who don't want the dashboard long-lived can set
91
94
  # CLAUDE_SMART_DASHBOARD_AUTOSTART=0 in their environment.
92
95
  if [ "${CLAUDE_SMART_DASHBOARD_AUTOSTART:-1}" = "0" ]; then
@@ -100,10 +103,19 @@ case "$CMD" in
100
103
  fi
101
104
  NPM_BIN=$(claude_smart_resolve_npm || true)
102
105
  if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
103
- reason="npm is not on PATH; dashboard cannot start"
104
- echo "[claude-smart] dashboard: $reason; skipping" >>"$LOG_FILE"
105
- claude_smart_write_dashboard_unavailable "$reason"
106
- emit_ok; exit 0
106
+ if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
107
+ echo "[claude-smart] dashboard: npm is not on PATH; running installer" >>"$LOG_FILE"
108
+ CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true
109
+ claude_smart_source_login_path
110
+ claude_smart_prepend_node_bins
111
+ NPM_BIN=$(claude_smart_resolve_npm || true)
112
+ fi
113
+ if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
114
+ reason="npm is not on PATH after installer; dashboard cannot start"
115
+ echo "[claude-smart] dashboard: $reason; skipping" >>"$LOG_FILE"
116
+ claude_smart_write_dashboard_unavailable "$reason"
117
+ emit_ok; exit 0
118
+ fi
107
119
  fi
108
120
 
109
121
  # `npm run start` requires a prior `next build`. Do NOT build in the
@@ -26,6 +26,10 @@ fi
26
26
  HERE="$(cd "$(dirname "$0")" && pwd)"
27
27
  # shellcheck source=_lib.sh
28
28
  . "$HERE/_lib.sh"
29
+ if claude_smart_is_internal_invocation_env; then
30
+ echo '{"continue":true,"suppressOutput":true}'
31
+ exit 0
32
+ fi
29
33
  # Pick up uv from the user's login-shell PATH (covers ~/.local/bin populated
30
34
  # by the astral.sh installer) so a fresh install works before the user
31
35
  # restarts their terminal. Matches the pattern used by smart-install.sh.
@@ -37,11 +41,22 @@ claude_smart_prepend_astral_bins
37
41
  PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
38
42
 
39
43
  FAILURE_MARKER="$HOME/.claude-smart/install-failed"
44
+ STATE_DIR="$HOME/.claude-smart"
40
45
  if [ -f "$FAILURE_MARKER" ]; then
41
- if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then
42
- python3 - "$FAILURE_MARKER" <<'PY'
46
+ # Auto-clear the marker when install inputs have changed since the
47
+ # failure was recorded (pyproject.toml / uv.lock / smart-install.sh /
48
+ # python interpreter etc.). Without this the plugin stays muted forever
49
+ # after a transient sync failure even when the user has fixed it.
50
+ stored_fp="$(awk -F= '$1=="fingerprint"{print $2; exit}' "$FAILURE_MARKER" 2>/dev/null || true)"
51
+ current_fp="$(claude_smart_install_fingerprint_hash "$PLUGIN_ROOT" "$HERE" 2>/dev/null || true)"
52
+ if [ -z "$stored_fp" ] || [ "$stored_fp" != "$current_fp" ]; then
53
+ rm -f "$FAILURE_MARKER"
54
+ else
55
+ if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then
56
+ python3 - "$FAILURE_MARKER" <<'PY'
43
57
  import json, pathlib, sys
44
- msg = pathlib.Path(sys.argv[1]).read_text().strip() or "unknown error"
58
+ first = pathlib.Path(sys.argv[1]).read_text().splitlines()
59
+ msg = (first[0].strip() if first else "") or "unknown error"
45
60
  print(json.dumps({
46
61
  "hookSpecificOutput": {
47
62
  "hookEventName": "SessionStart",
@@ -54,16 +69,42 @@ print(json.dumps({
54
69
  }
55
70
  }))
56
71
  PY
57
- else
58
- echo '{"continue":true,"suppressOutput":true}'
72
+ else
73
+ echo '{"continue":true,"suppressOutput":true}'
74
+ fi
75
+ exit 0
59
76
  fi
60
- exit 0
61
77
  fi
62
78
 
63
79
  if ! command -v uv >/dev/null 2>&1; then
64
- # uv missing post-install don't crash the session, just no-op.
65
- echo '{"continue":true,"suppressOutput":true}'
66
- exit 0
80
+ # Self-heal from skipped Setup/SessionStart bootstrap. SessionStart can
81
+ # afford to wait because it has the install budget; prompt/tool hooks start
82
+ # the same installer detached so normal work is not blocked by first-run
83
+ # dependency setup.
84
+ if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then
85
+ echo '{"continue":true,"suppressOutput":true}'
86
+ exit 0
87
+ fi
88
+ if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
89
+ mkdir -p "$STATE_DIR"
90
+ if [ "$EVENT" = "session-start" ]; then
91
+ CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2
92
+ claude_smart_prepend_astral_bins
93
+ claude_smart_prepend_node_bins
94
+ if command -v uv >/dev/null 2>&1; then
95
+ bash "$HERE/backend-service.sh" start >/dev/null 2>&1 || true
96
+ bash "$HERE/dashboard-service.sh" start >/dev/null 2>&1 || true
97
+ fi
98
+ else
99
+ claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
100
+ bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
101
+ >>"$STATE_DIR/install.log" 2>&1 || true
102
+ fi
103
+ fi
104
+ if ! command -v uv >/dev/null 2>&1; then
105
+ echo '{"continue":true,"suppressOutput":true}'
106
+ exit 0
107
+ fi
67
108
  fi
68
109
 
69
110
  # Stdin is the hook payload JSON — stream it through to the Python CLI.