claude-smart 0.2.37 → 0.2.40

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
@@ -13,7 +13,7 @@
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.37-green.svg" alt="Version">
16
+ <img src="https://img.shields.io/badge/version-0.2.40-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">
@@ -46,33 +46,6 @@
46
46
 
47
47
  ---
48
48
 
49
- ## Why Learning, Not Memory
50
-
51
- Most memory tools just record. Claude remembers what happened, but doesn't change what it does next.
52
-
53
- `claude-smart` focuses on learning instead.
54
-
55
- Four things this changes:
56
-
57
- - 💡 **Stop repeating the same mistakes:** Produces actionable skills Claude can follow next time; memory only records what happened.
58
-
59
- > *Example:* a deploy fails after Claude bumps `prisma` from 5.x to 6.0; you tell it to roll back because 6.0 breaks nested writes in your order flow.<br>
60
- > **Memory:** “deploy broke after prisma bump; user rolled back”<br>
61
- > **Learning:** “treat major-version bumps of ORMs/DB drivers as breaking — verify with integration tests, not just unit tests”
62
-
63
- - 🚀 **Start from the optimized path:** Records and optimizes the steps that worked so Claude can reuse them, instead of re-exploring each session.
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>
65
- > **Memory:** “user mentioned that `npm run dev` did not work”<br>
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”
67
-
68
- Instead of re-exploring, Claude Code starts from the proven path—reducing planning steps, latency, and token usage.
69
-
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.
71
-
72
- - 🪶 **Better context without prompt bloat:** Distilled, deduplicated skills stay in dozens of tokens—not thousands—even as the project grows.
73
-
74
- ---
75
-
76
49
  ## Quick Start
77
50
 
78
51
  ### Claude Code
@@ -126,18 +99,38 @@ Developing the plugin itself? See [DEVELOPER.md](./DEVELOPER.md#developing-local
126
99
 
127
100
  > **Not supported:** Claude Code Cowork, claude.ai/code web, or remote Codex environments without local plugin hooks — they run outside your local machine, so the local backend/dashboard and `~/.reflexio/` aren't reachable.
128
101
 
129
- ### Vanilla OS support
102
+ ---
103
+
104
+ ## Why Learning, Not Memory
130
105
 
131
- | Platform | Status | Notes |
132
- | --- | --- | --- |
133
- | Apple Silicon macOS 14+ | Supported | Runtime bootstrap installs private Node/npm, uv, Python 3.12 deps, and dashboard deps. |
134
- | Windows x64 | Supported | Runtime bootstrap uses PowerShell for uv/Node archive extraction and patches Codex hooks to the Node wrapper. |
135
- | Linux | Supported when host hooks are local | Existing Linux behavior is preserved; install coverage depends on available Python wheels. |
136
- | Intel Mac, macOS 13 or older, Windows ARM | Not supported | Current local embedding/ML dependencies do not publish a complete native wheel set for these targets. |
106
+ Most memory tools just record. Claude remembers what happened, but doesn't change what it does next.
107
+
108
+ `claude-smart` focuses on learning instead.
109
+
110
+ Four things this changes:
111
+
112
+ - 💡 **Stop repeating the same mistakes:** Produces actionable skills Claude can follow next time; memory only records what happened.
113
+
114
+ > *Example:* a deploy fails after Claude bumps `prisma` from 5.x to 6.0; you tell it to roll back because 6.0 breaks nested writes in your order flow.<br>
115
+ > **Memory:** “deploy broke after prisma bump; user rolled back”<br>
116
+ > **Learning:** “treat major-version bumps of ORMs/DB drivers as breaking — verify with integration tests, not just unit tests”
117
+
118
+ - 🚀 **Start from the optimized path:** Records and optimizes the steps that worked so Claude can reuse them, instead of re-exploring each session.
119
+ > *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>
120
+ > **Memory:** “user mentioned that `npm run dev` did not work”<br>
121
+ > **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”
122
+
123
+ Instead of re-exploring, Claude Code starts from the proven path—reducing planning steps, latency, and token usage.
124
+
125
+ - 🌐 **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.
126
+
127
+ - 🪶 **Better context without prompt bloat:** Distilled, deduplicated skills stay in dozens of tokens—not thousands—even as the project grows.
128
+
129
+ ---
130
+
131
+ ## Demo
137
132
 
138
- Network access is required during first install for npm, PyPI/uv, Node.js, and
139
- the first local embedding model download. The ONNX model cache lives at
140
- `~/.cache/chroma/onnx_models/all-MiniLM-L6-v2/`.
133
+ https://github.com/user-attachments/assets/ed84d73a-d5b7-4a0d-ab2c-f2c060c3f276
141
134
 
142
135
  ---
143
136
 
@@ -231,7 +224,7 @@ For troubleshooting, see [TROUBLESHOOTING.md](./TROUBLESHOOTING.md).
231
224
 
232
225
  ## License
233
226
 
234
- This project is licensed under the **Apache License 2.0**. The bundled `reflexio/` submodule is also Apache 2.0. Claude Code is Anthropic's and not covered by this license.
227
+ This project is licensed under the **Apache License 2.0**. Reflexio is a separate Apache 2.0 project consumed as the `reflexio-ai` Python package. Claude Code is Anthropic's and not covered by this license.
235
228
 
236
229
  See the [LICENSE](LICENSE) file for details.
237
230
 
@@ -16,6 +16,7 @@
16
16
  const { execSync, spawn, spawnSync } = require("child_process");
17
17
  const crypto = require("crypto");
18
18
  const {
19
+ chmodSync,
19
20
  cpSync,
20
21
  existsSync,
21
22
  lstatSync,
@@ -41,6 +42,8 @@ const REFLEXIO_ENV_PATH = join(homedir(), ".reflexio", ".env");
41
42
  const MANAGED_REFLEXIO_URL = "https://www.reflexio.ai/";
42
43
  const MANAGED_SETUP_ENV = "CLAUDE_SMART_MANAGED_SETUP";
43
44
  const CLAUDE_SMART_READ_ONLY_ENV = "CLAUDE_SMART_READ_ONLY";
45
+ const CLAUDE_SMART_USE_LOCAL_CLI_ENV = "CLAUDE_SMART_USE_LOCAL_CLI";
46
+ const CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV = "CLAUDE_SMART_USE_LOCAL_EMBEDDING";
44
47
  const REFLEXIO_USER_ID_ENV = "REFLEXIO_USER_ID";
45
48
  const REFLEXIO_DIR = join(homedir(), ".reflexio");
46
49
  const CLAUDE_SMART_STATE_DIR = join(homedir(), ".claude-smart");
@@ -90,6 +93,24 @@ const COPYTREE_IGNORE_NAMES = new Set([
90
93
  "node_modules",
91
94
  ".next",
92
95
  ]);
96
+ const LOCAL_DEFAULT_ENV_ENTRIES = [
97
+ [
98
+ "# Route reflexio generation through the local Claude Code CLI",
99
+ CLAUDE_SMART_USE_LOCAL_CLI_ENV,
100
+ "1",
101
+ ],
102
+ [
103
+ "# Use the in-process ONNX embedder (chromadb) - no API key for semantic search",
104
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV,
105
+ "1",
106
+ ],
107
+ [null, CLAUDE_SMART_READ_ONLY_ENV, "0"],
108
+ ];
109
+ const LOCAL_MODE_PRUNE_KEYS = new Set([
110
+ "REFLEXIO_URL",
111
+ "REFLEXIO_API_KEY",
112
+ REFLEXIO_USER_ID_ENV,
113
+ ]);
93
114
 
94
115
  function shouldCopyPath(src) {
95
116
  const base = src.split(/[\\/]/).pop() || "";
@@ -216,6 +237,57 @@ function parseEnvLine(line) {
216
237
  return { key, value };
217
238
  }
218
239
 
240
+ function escapeEnvValue(value) {
241
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
242
+ }
243
+
244
+ function ensureLocalReflexioEnv() {
245
+ mkdirSync(dirname(REFLEXIO_ENV_PATH), { recursive: true });
246
+ const existing = existsSync(REFLEXIO_ENV_PATH)
247
+ ? readFileSync(REFLEXIO_ENV_PATH, "utf8")
248
+ : "";
249
+ const present = new Set();
250
+ const keptLines = [];
251
+ let pruned = false;
252
+ for (const line of existing.split(/\r?\n/)) {
253
+ const parsed = parseEnvLine(line);
254
+ if (parsed) {
255
+ if (LOCAL_MODE_PRUNE_KEYS.has(parsed.key)) {
256
+ pruned = true;
257
+ continue;
258
+ }
259
+ present.add(parsed.key);
260
+ }
261
+ keptLines.push(line);
262
+ }
263
+
264
+ const additions = [];
265
+ const added = [];
266
+ for (const [comment, key, value] of LOCAL_DEFAULT_ENV_ENTRIES) {
267
+ if (present.has(key)) continue;
268
+ if (comment) additions.push(comment);
269
+ if (key === CLAUDE_SMART_READ_ONLY_ENV) {
270
+ additions.push(`${key}="${escapeEnvValue(value)}"`);
271
+ } else {
272
+ additions.push(`${key}=${escapeEnvValue(value)}`);
273
+ }
274
+ added.push(key);
275
+ }
276
+
277
+ if (additions.length > 0 || pruned) {
278
+ let content = keptLines.join("\n").replace(/\n*$/, "");
279
+ if (additions.length > 0) {
280
+ const prefix = content ? "\n" : "";
281
+ content = content + prefix + additions.join("\n");
282
+ }
283
+ writeFileSync(REFLEXIO_ENV_PATH, content ? `${content}\n` : "");
284
+ } else if (!existsSync(REFLEXIO_ENV_PATH)) {
285
+ writeFileSync(REFLEXIO_ENV_PATH, "");
286
+ }
287
+ chmodSync(REFLEXIO_ENV_PATH, 0o600);
288
+ return added;
289
+ }
290
+
219
291
  function maskSecret(value) {
220
292
  if (!value) return "";
221
293
  if (value.length <= 8) return "*".repeat(value.length);
@@ -254,7 +326,12 @@ function loadReflexioSetupEnv() {
254
326
  } else {
255
327
  delete process.env.REFLEXIO_URL;
256
328
  delete process.env.REFLEXIO_API_KEY;
329
+ delete process.env[REFLEXIO_USER_ID_ENV];
257
330
  delete process.env[MANAGED_SETUP_ENV];
331
+ const added = ensureLocalReflexioEnv();
332
+ if (added.length > 0) {
333
+ process.stdout.write(`Seeded ${REFLEXIO_ENV_PATH} with ${added.join(", ")}.\n`);
334
+ }
258
335
  }
259
336
  const readOnly = ["1", "true", "yes", "on"].includes(
260
337
  String(readOnlyValue).trim().toLowerCase(),
@@ -795,6 +872,36 @@ function ensurePluginRoot(pluginRoot) {
795
872
  }
796
873
  }
797
874
 
875
+ async function installVendoredReflexio(pluginRoot, uv, env) {
876
+ const vendorRoot = join(pluginRoot, "vendor", "reflexio");
877
+ if (!existsSync(join(vendorRoot, "pyproject.toml"))) return;
878
+
879
+ const pythonPath = isWindows()
880
+ ? join(pluginRoot, ".venv", "Scripts", "python.exe")
881
+ : join(pluginRoot, ".venv", "bin", "python");
882
+ if (!existsSync(pythonPath)) {
883
+ throw new Error(`plugin Python was not created by uv sync: ${pythonPath}`);
884
+ }
885
+
886
+ process.stdout.write(`Installing bundled Reflexio source from ${vendorRoot}...\n`);
887
+ let code = await runChecked(
888
+ uv,
889
+ ["pip", "install", "--project", pluginRoot, "--python", pythonPath, "--quiet", "-e", vendorRoot],
890
+ { cwd: pluginRoot, env },
891
+ );
892
+ if (code !== 0) {
893
+ process.stderr.write(
894
+ `warning: quiet vendored Reflexio install failed in ${pluginRoot}; retrying with full output.\n`,
895
+ );
896
+ code = await runChecked(
897
+ uv,
898
+ ["pip", "install", "--project", pluginRoot, "--python", pythonPath, "-e", vendorRoot],
899
+ { cwd: pluginRoot, env },
900
+ );
901
+ }
902
+ if (code !== 0) throw new Error(`vendored Reflexio install failed in ${pluginRoot}`);
903
+ }
904
+
798
905
  async function bootstrapPluginRuntime(pluginRoot, options = {}) {
799
906
  assertSupportedRuntimePlatform();
800
907
  process.stdout.write("Preparing claude-smart runtime for hooks...\n");
@@ -830,6 +937,7 @@ async function bootstrapPluginRuntime(pluginRoot, options = {}) {
830
937
  );
831
938
  }
832
939
  if (code !== 0) throw new Error(`uv sync failed in ${pluginRoot}`);
940
+ await installVendoredReflexio(pluginRoot, uv, env);
833
941
 
834
942
  const dashboardDir = join(pluginRoot, "dashboard");
835
943
  if (existsSync(dashboardDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.37",
3
+ "version": "0.2.40",
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.37",
3
+ "version": "0.2.40",
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.37",
3
+ "version": "0.2.40",
4
4
  "description": "Self-improving coding assistant plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -1,16 +1,24 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.37"
3
+ version = "0.2.40"
4
4
  description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  dependencies = [
8
- "reflexio-ai>=0.2.22",
9
- # Used by reflexio's local embedding provider (ONNXMiniLM_L6_V2).
8
+ # Core Reflexio client/backend package. This is the PyPI baseline used by
9
+ # normal installs; vendor releases may override it after uv sync by
10
+ # installing plugin/vendor/reflexio. Keep in sync via
11
+ # scripts/sync-reflexio-dep.py.
12
+ "reflexio-ai>=0.2.23",
13
+ # Needed when Reflexio runs the local embedding provider (ONNXMiniLM_L6_V2).
10
14
  # Pulls in onnxruntime + tokenizers; the ~80 MB ONNX model itself is
11
15
  # downloaded on first use, not at install time.
12
16
  "chromadb>=0.5",
13
17
  "einops>=0.8.0",
18
+ # Native SQLite vector KNN for local storage. Reflexio can fall back to
19
+ # Python ranking without this, but installed claude-smart should include
20
+ # the accelerator by default.
21
+ "sqlite-vec>=0.1.6",
14
22
  ]
15
23
 
16
24
  [project.scripts]
@@ -18,14 +26,6 @@ claude-smart-hook = "claude_smart.hook:main"
18
26
  claude-smart = "claude_smart.cli:main"
19
27
  claude-smart-optimizer-assistant = "claude_smart.optimizer_assistant:main"
20
28
 
21
- # NOTE: LOCAL DEV ONLY — do not commit this block to main.
22
- # Published installs must resolve reflexio-ai from PyPI. This override
23
- # points at the vendored submodule so editable changes flow through.
24
- # `git update-index --skip-worktree plugin/pyproject.toml plugin/uv.lock`
25
- # hides the local divergence from `git status`.
26
- # [tool.uv.sources]
27
- # reflexio-ai = { path = "/Users/yilu/repos/claude-smart/reflexio", editable = true }
28
-
29
29
  [build-system]
30
30
  requires = ["hatchling"]
31
31
  build-backend = "hatchling.build"
@@ -196,6 +196,30 @@ claude_smart_resolve_python() {
196
196
  return 1
197
197
  }
198
198
 
199
+ claude_smart_plugin_python() {
200
+ local plugin_root
201
+ plugin_root="$1"
202
+ if claude_smart_is_windows; then
203
+ printf '%s\n' "$plugin_root/.venv/Scripts/python.exe"
204
+ else
205
+ printf '%s\n' "$plugin_root/.venv/bin/python"
206
+ fi
207
+ }
208
+
209
+ claude_smart_python_imports() {
210
+ local plugin_root module python_bin
211
+ plugin_root="$1"
212
+ module="$2"
213
+ python_bin="$(claude_smart_plugin_python "$plugin_root")"
214
+ [ -x "$python_bin" ] || return 1
215
+ "$python_bin" - "$module" <<'PY' >/dev/null 2>&1
216
+ import importlib
217
+ import sys
218
+
219
+ importlib.import_module(sys.argv[1])
220
+ PY
221
+ }
222
+
199
223
  claude_smart_download() {
200
224
  local url dest src _CS_PY
201
225
  url="$1"
@@ -298,6 +322,8 @@ claude_smart_install_fingerprint() {
298
322
  printf 'lib=%s\n' "$(claude_smart_fingerprint_file "$script_dir/_lib.sh")"
299
323
  printf 'pyproject=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/pyproject.toml")"
300
324
  printf 'uv_lock=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/uv.lock")"
325
+ printf 'vendor_reflexio_pyproject=%s\n' \
326
+ "$(claude_smart_fingerprint_file "$plugin_root/vendor/reflexio/pyproject.toml")"
301
327
  # Resolved python interpreter — catches a system upgrade (3.12.4 → 3.12.5)
302
328
  # that would otherwise let install_complete return true against a venv
303
329
  # built against a now-deleted interpreter.
@@ -220,6 +220,21 @@ case "$CMD" in
220
220
  export INTERACTION_CLEANUP_THRESHOLD="${INTERACTION_CLEANUP_THRESHOLD:-500}"
221
221
  export INTERACTION_CLEANUP_DELETE_COUNT="${INTERACTION_CLEANUP_DELETE_COUNT:-200}"
222
222
 
223
+ # Keep plugin runtime data in ~/.reflexio even when the backend imports
224
+ # Reflexio from an editable checkout inside a larger repo with its own
225
+ # .env. python-dotenv respects pre-existing env vars, so this prevents a
226
+ # parent REFLEXIO_LOG_DIR from sending claude-smart to enterprise configs.
227
+ export REFLEXIO_LOG_DIR="${REFLEXIO_LOG_DIR:-$HOME}"
228
+
229
+ # Force sqlite: the plugin venv ships only the open-source reflexio
230
+ # package, which doesn't register the Supabase/Postgres storage
231
+ # factories. If the user's ~/.reflexio/.env sets REFLEXIO_STORAGE to
232
+ # something else (common when sharing the file with reflexio_ext),
233
+ # the backend boots but crashes every request. load_dotenv() inside
234
+ # the CLI respects pre-existing env vars, so exporting here wins
235
+ # without touching the file on disk.
236
+ export REFLEXIO_STORAGE="sqlite"
237
+
223
238
  # (nohup; no process groups). backend-log-runner.sh owns stdout/stderr
224
239
  # capture so process output cannot grow backend.log past its cap.
225
240
  #
@@ -233,7 +248,8 @@ case "$CMD" in
233
248
  workers="${CLAUDE_SMART_BACKEND_WORKERS:-1}"
234
249
  claude_smart_spawn_detached bash "$HERE/backend-log-runner.sh" \
235
250
  "$LOG_FILE" "$LOG_MAX_BYTES" -- \
236
- uv run --project "$PLUGIN_ROOT" --quiet \
251
+ env PYTHONIOENCODING="${PYTHONIOENCODING:-utf-8}" \
252
+ uv run --project "$PLUGIN_ROOT" --no-sync --quiet \
237
253
  reflexio services start --only backend --no-reload --workers "$workers"
238
254
  svc_pid=$!
239
255
  # Record the spawned pid, not a pgid sampled with ps. On POSIX,
@@ -63,4 +63,4 @@ if ! command -v uv >/dev/null 2>&1; then
63
63
  fi
64
64
  fi
65
65
 
66
- exec uv run --project "$PLUGIN_ROOT" --quiet python -m claude_smart.cli "$@"
66
+ exec uv run --project "$PLUGIN_ROOT" --no-sync --quiet python -m claude_smart.cli "$@"
@@ -535,7 +535,7 @@ function runHook(root, event) {
535
535
  REFLEXIO_URL: readBackendUrl(),
536
536
  CLAUDE_SMART_HOST: "codex",
537
537
  CLAUDE_SMART_CLI_PATH: process.env.CLAUDE_SMART_CLI_PATH || codexCompatPath(root),
538
- CLAUDE_SMART_CITATION_LINK_STYLE: process.env.CLAUDE_SMART_CITATION_LINK_STYLE || "osc8",
538
+ CLAUDE_SMART_CITATION_LINK_STYLE: process.env.CLAUDE_SMART_CITATION_LINK_STYLE || "markdown",
539
539
  },
540
540
  input,
541
541
  stdio: ["pipe", "pipe", "inherit"],
@@ -3,7 +3,8 @@
3
3
  # CLAUDE_PLUGIN_ROOT points at the plugin dir (dev: <repo>/plugin;
4
4
  # installed: ~/.claude/plugins/cache/reflexioai/claude-smart/<version>),
5
5
  # which is also the Python project root with pyproject.toml + uv.lock.
6
- # We invoke via `uv run --project` so the pinned env from uv.lock is used.
6
+ # We invoke via `uv run --project --no-sync` so the prepared env is used without
7
+ # undoing editable Reflexio installs from local development.
7
8
  #
8
9
  # If the Setup hook recorded an install failure at
9
10
  # ~/.claude-smart/install-failed, short-circuit with a user-visible
@@ -20,7 +21,7 @@ case "$EVENT" in
20
21
  esac
21
22
  export CLAUDE_SMART_HOST="$HOST"
22
23
  if [ "$HOST" = "codex" ] && [ -z "${CLAUDE_SMART_CITATION_LINK_STYLE:-}" ]; then
23
- export CLAUDE_SMART_CITATION_LINK_STYLE="osc8"
24
+ export CLAUDE_SMART_CITATION_LINK_STYLE="markdown"
24
25
  fi
25
26
 
26
27
  HERE="$(cd "$(dirname "$0")" && pwd)"
@@ -58,8 +59,9 @@ if [ -f "$FAILURE_MARKER" ]; then
58
59
  if [ -z "$stored_fp" ] || [ "$stored_fp" != "$current_fp" ]; then
59
60
  rm -f "$FAILURE_MARKER"
60
61
  else
61
- if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then
62
- python3 - "$FAILURE_MARKER" <<'PY'
62
+ failure_py="$(claude_smart_resolve_python 2>/dev/null || true)"
63
+ if [ "$EVENT" = "session-start" ] && [ -n "$failure_py" ]; then
64
+ "$failure_py" - "$FAILURE_MARKER" <<'PY'
63
65
  import json, pathlib, sys
64
66
  first = pathlib.Path(sys.argv[1]).read_text().splitlines()
65
67
  msg = (first[0].strip() if first else "") or "unknown error"
@@ -113,5 +115,38 @@ if ! command -v uv >/dev/null 2>&1; then
113
115
  fi
114
116
  fi
115
117
 
118
+ ensure_hook_package_importable() {
119
+ if claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.hook; then
120
+ return 0
121
+ fi
122
+
123
+ if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then
124
+ return 1
125
+ fi
126
+ if [ ! -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
127
+ return 1
128
+ fi
129
+
130
+ mkdir -p "$STATE_DIR"
131
+ if [ "$EVENT" = "session-start" ]; then
132
+ CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2
133
+ else
134
+ {
135
+ printf '%s\n' "[claude-smart] hook: claude_smart.hook is not importable; retrying install in background"
136
+ date 2>/dev/null || true
137
+ } >>"$STATE_DIR/install.log" 2>&1
138
+ claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
139
+ bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
140
+ >>"$STATE_DIR/install.log" 2>&1 || true
141
+ fi
142
+
143
+ claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.hook
144
+ }
145
+
146
+ if ! ensure_hook_package_importable; then
147
+ claude_smart_emit_continue
148
+ exit 0
149
+ fi
150
+
116
151
  # Stdin is the hook payload JSON — stream it through to the Python CLI.
117
- exec uv run --project "$PLUGIN_ROOT" --quiet python -m claude_smart.hook "$HOST" "$EVENT"
152
+ exec uv run --project "$PLUGIN_ROOT" --no-sync --quiet python -m claude_smart.hook "$HOST" "$EVENT"
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
- # Run once on plugin install. Pulls the reflexio submodule, syncs the
3
- # Python env, and flips on the claude-code LiteLLM provider in reflexio's
4
- # .env so extraction works with no external API key.
2
+ # Run once on plugin install. Syncs the Python env and flips on the
3
+ # claude-code LiteLLM provider in reflexio's .env so extraction works with no
4
+ # external API key.
5
5
  #
6
6
  # On failure, writes the reason to ~/.claude-smart/install-failed so
7
7
  # hook_entry.sh can short-circuit and surface a user-visible message
@@ -105,12 +105,45 @@ install_complete() {
105
105
  [ "$(cat "$SUCCESS_MARKER" 2>/dev/null || true)" = "$(install_fingerprint)" ] || return 1
106
106
  command -v uv >/dev/null 2>&1 || return 1
107
107
  [ -d "$PLUGIN_ROOT/.venv" ] || return 1
108
+ claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.hook || return 1
109
+ if [ -z "${REFLEXIO_API_KEY:-}" ]; then
110
+ [ -f "$HOME/.reflexio/.env" ] || return 1
111
+ grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.reflexio/.env" || return 1
112
+ grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.reflexio/.env" || return 1
113
+ grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_READ_ONLY=' "$HOME/.reflexio/.env" || return 1
114
+ fi
108
115
  if [ -d "$PLUGIN_ROOT/dashboard" ]; then
109
116
  [ -d "$PLUGIN_ROOT/dashboard/.next" ] || [ -f "$MARKER_DIR/dashboard-build.pid" ] || [ -f "$(claude_smart_dashboard_unavailable_marker)" ] || return 1
110
117
  fi
111
118
  return 0
112
119
  }
113
120
 
121
+ install_vendored_reflexio() {
122
+ local VENDORED_REFLEXIO PLUGIN_PYTHON
123
+
124
+ VENDORED_REFLEXIO="$PLUGIN_ROOT/vendor/reflexio"
125
+ [ -f "$VENDORED_REFLEXIO/pyproject.toml" ] || return 0
126
+
127
+ if claude_smart_is_windows; then
128
+ PLUGIN_PYTHON="$PLUGIN_ROOT/.venv/Scripts/python.exe"
129
+ else
130
+ PLUGIN_PYTHON="$PLUGIN_ROOT/.venv/bin/python"
131
+ fi
132
+ if [ ! -x "$PLUGIN_PYTHON" ]; then
133
+ write_failure "plugin Python was not created by uv sync: $PLUGIN_PYTHON"
134
+ fi
135
+
136
+ echo "[claude-smart] installing bundled Reflexio source from $VENDORED_REFLEXIO" >&2
137
+ if uv pip install --project "$PLUGIN_ROOT" --python "$PLUGIN_PYTHON" --quiet -e "$VENDORED_REFLEXIO" >&2; then
138
+ return 0
139
+ fi
140
+
141
+ echo "[claude-smart] warning: quiet vendored Reflexio install failed in $PLUGIN_ROOT; retrying with full output." >&2
142
+ if ! uv pip install --project "$PLUGIN_ROOT" --python "$PLUGIN_PYTHON" -e "$VENDORED_REFLEXIO" >&2; then
143
+ write_failure "vendored Reflexio install failed in $PLUGIN_ROOT"
144
+ fi
145
+ }
146
+
114
147
  write_success_marker() {
115
148
  install_fingerprint > "$SUCCESS_MARKER"
116
149
  }
@@ -362,19 +395,6 @@ if install_complete; then
362
395
  exit 0
363
396
  fi
364
397
 
365
- # Dev-mode only: when running from a git checkout, pull the reflexio
366
- # submodule so tests/benchmarks can use its sources. In install mode the
367
- # plugin lives under ~/.claude/plugins/cache and reflexio-ai resolves
368
- # from PyPI instead. The guard checks for both `.git` and `.gitmodules`
369
- # at REPO_ROOT to distinguish a dev checkout from a marketplace cache
370
- # (where REPO_ROOT has neither).
371
- if [ -d "$REPO_ROOT/.git" ] && [ -f "$REPO_ROOT/.gitmodules" ]; then
372
- echo "[claude-smart] initializing reflexio submodule..." >&2
373
- if ! (cd "$REPO_ROOT" && git submodule update --init --recursive reflexio) >&2; then
374
- echo "[claude-smart] WARNING: git submodule update failed; continuing with PyPI reflexio-ai" >&2
375
- fi
376
- fi
377
-
378
398
  if ! command -v uv >/dev/null 2>&1; then
379
399
  echo "[claude-smart] uv not found — installing from astral.sh..." >&2
380
400
  # The astral.sh bash installer downloads a zip and unzips it. On
@@ -422,6 +442,18 @@ echo "[claude-smart] running uv sync..." >&2
422
442
  if ! uv sync --locked --python 3.12 --quiet >&2; then
423
443
  write_failure "uv sync failed in $PLUGIN_ROOT — run 'uv sync --locked --python 3.12' there to diagnose"
424
444
  fi
445
+ # Defend against a stale lockfile silently dropping the project install.
446
+ # When plugin/uv.lock pins a different claude-smart version than pyproject.toml,
447
+ # `uv sync --locked` exits 0 but does NOT install the claude_smart package, so
448
+ # every hook fails later with `ModuleNotFoundError: No module named 'claude_smart'`.
449
+ # Catch that here with a clear, user-visible install-failed marker.
450
+ if ! claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.hook; then
451
+ write_failure "uv sync left claude_smart uninstalled in $PLUGIN_ROOT/.venv — plugin/uv.lock is likely out of sync with pyproject.toml (regenerate with 'uv lock --project plugin' and reinstall the plugin)"
452
+ fi
453
+ install_vendored_reflexio
454
+ if ! claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.hook; then
455
+ write_failure "claude-smart package is not importable after vendored Reflexio install in $PLUGIN_ROOT/.venv"
456
+ fi
425
457
 
426
458
  # Reflexio's CLI reads ~/.reflexio/.env (see reflexio/cli/env_loader.py);
427
459
  # append our two opt-in flags there so `reflexio services start` picks
@@ -465,6 +497,63 @@ claude_smart_env_upsert() {
465
497
  fi
466
498
  }
467
499
 
500
+ claude_smart_env_append_raw_if_missing() {
501
+ local key value
502
+ key="$1"
503
+ value="$2"
504
+ if ! grep -qE "^(export[[:space:]]+)?${key}=" "$REFLEXIO_ENV"; then
505
+ printf '%s=%s\n' "$key" "$value" >> "$REFLEXIO_ENV"
506
+ fi
507
+ }
508
+
509
+ claude_smart_prune_managed_env_keys_for_local() {
510
+ local tmp line raw_key key
511
+ [ -f "$REFLEXIO_ENV" ] || return 0
512
+ tmp="$(mktemp "${REFLEXIO_ENV}.tmp.XXXXXX")"
513
+ while IFS= read -r line || [ -n "$line" ]; do
514
+ raw_key="$line"
515
+ raw_key="${raw_key#"${raw_key%%[![:space:]]*}"}"
516
+ case "$raw_key" in
517
+ export\ *) raw_key="${raw_key#export }" ;;
518
+ esac
519
+ key="${raw_key%%=*}"
520
+ key="${key%"${key##*[![:space:]]}"}"
521
+ case "$key" in
522
+ REFLEXIO_URL|REFLEXIO_API_KEY|REFLEXIO_USER_ID)
523
+ continue
524
+ ;;
525
+ esac
526
+ printf '%s\n' "$line" >> "$tmp"
527
+ done < "$REFLEXIO_ENV"
528
+ mv "$tmp" "$REFLEXIO_ENV"
529
+ }
530
+
531
+ claude_smart_ensure_local_env_defaults() {
532
+ [ -z "${REFLEXIO_API_KEY:-}" ] || return 0
533
+ mkdir -p "$(dirname "$REFLEXIO_ENV")"
534
+ touch "$REFLEXIO_ENV"
535
+ chmod 600 "$REFLEXIO_ENV"
536
+ claude_smart_prune_managed_env_keys_for_local
537
+ unset REFLEXIO_URL REFLEXIO_API_KEY REFLEXIO_USER_ID CLAUDE_SMART_MANAGED_SETUP
538
+ if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
539
+ printf '# Route reflexio generation through the local Claude Code CLI\n' >> "$REFLEXIO_ENV"
540
+ claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_CLI "1"
541
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
542
+ fi
543
+ if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
544
+ printf '# Use the in-process ONNX embedder (chromadb) - no API key for semantic search\n' >> "$REFLEXIO_ENV"
545
+ claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_EMBEDDING "1"
546
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
547
+ fi
548
+ if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_READ_ONLY=' "$REFLEXIO_ENV"; then
549
+ claude_smart_env_upsert CLAUDE_SMART_READ_ONLY "0"
550
+ echo "[claude-smart] appended CLAUDE_SMART_READ_ONLY=0 to $REFLEXIO_ENV" >&2
551
+ fi
552
+ chmod 600 "$REFLEXIO_ENV"
553
+ }
554
+
555
+ claude_smart_ensure_local_env_defaults
556
+
468
557
  # Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
469
558
  # plugin backend port (8071). Matches the quoted and unquoted forms but
470
559
  # requires paired quotes, so malformed or deliberately different values