claude-smart 0.2.48 → 0.2.49

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 (50) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +6 -2
  3. package/bin/claude-smart.js +289 -56
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/opencode/dist/server.mjs +60 -2
  8. package/plugin/opencode/server.mts +64 -2
  9. package/plugin/pyproject.toml +2 -2
  10. package/plugin/scripts/_lib.sh +58 -6
  11. package/plugin/scripts/backend-service.sh +15 -10
  12. package/plugin/scripts/codex-hook.js +16 -4
  13. package/plugin/scripts/dashboard-service.sh +1 -1
  14. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  15. package/plugin/scripts/opencode-claude-compat.js +8 -1
  16. package/plugin/scripts/smart-install.sh +101 -20
  17. package/plugin/src/README.md +1 -1
  18. package/plugin/src/claude_smart/cli.py +79 -29
  19. package/plugin/src/claude_smart/env_config.py +53 -11
  20. package/plugin/uv.lock +5 -5
  21. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  22. package/plugin/vendor/reflexio/reflexio/client/client.py +40 -6
  23. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +18 -0
  24. package/plugin/vendor/reflexio/reflexio/server/api.py +15 -4
  25. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +1 -1
  26. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
  27. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +183 -1
  28. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
  29. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  30. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  31. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +193 -0
  32. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
  33. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +150 -37
  34. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +44 -22
  35. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  36. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +140 -2
  37. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +379 -0
  38. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
  39. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  40. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
  41. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +16 -7
  42. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  43. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +12 -5
  44. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +52 -7
  45. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +28 -4
  46. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
  47. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  48. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +219 -0
  49. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +23 -1
  50. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +22 -0
@@ -85,6 +85,8 @@ write_failure() {
85
85
  local reason fp_hash
86
86
  reason="$1"
87
87
  fp_hash="$(claude_smart_install_fingerprint_hash "$PLUGIN_ROOT" "$HERE" 2>/dev/null || true)"
88
+ # bin/claude-smart.js reads this marker too: first line is the user-facing
89
+ # failure reason, and fingerprint=... marks which install attempt produced it.
88
90
  {
89
91
  printf '%s\n' "$reason"
90
92
  if [ -n "$fp_hash" ]; then
@@ -97,6 +99,22 @@ write_failure() {
97
99
  exit 0
98
100
  }
99
101
 
102
+ verify_windows_local_embedding_runtime() {
103
+ claude_smart_is_windows || return 0
104
+ [ "${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}" = "1" ] || return 0
105
+ if claude_smart_python_imports "$PLUGIN_ROOT" onnxruntime; then
106
+ return 0
107
+ fi
108
+ write_failure "Windows local embedding requires Microsoft Visual C++ Redistributable for onnxruntime; install the x64 redistributable from https://aka.ms/vs/17/release/vc_redist.x64.exe and rerun claude-smart install."
109
+ }
110
+
111
+ if [ "${1:-}" = "verify-windows-embedding" ]; then
112
+ # Node bootstrap runs this after uv sync so install-failed markers stay
113
+ # consistent with the shell installer path.
114
+ verify_windows_local_embedding_runtime
115
+ exit 0
116
+ fi
117
+
100
118
  install_fingerprint() {
101
119
  claude_smart_install_fingerprint "$PLUGIN_ROOT" "$HERE"
102
120
  }
@@ -112,6 +130,7 @@ install_complete() {
112
130
  grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.claude-smart/.env" || return 1
113
131
  grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.claude-smart/.env" || return 1
114
132
  grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_READ_ONLY=' "$HOME/.claude-smart/.env" || return 1
133
+ grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_HOST=' "$HOME/.claude-smart/.env" || return 1
115
134
  fi
116
135
  if [ -d "$PLUGIN_ROOT/dashboard" ]; then
117
136
  [ -d "$PLUGIN_ROOT/dashboard/.next" ] || [ -f "$MARKER_DIR/dashboard-build.pid" ] || [ -f "$(claude_smart_dashboard_unavailable_marker)" ] || return 1
@@ -196,7 +215,7 @@ install_private_node() {
196
215
  local NODE_MIN_MAJOR NODE_MIN_MINOR NODE_LTS_MAJOR
197
216
  local node_os archive_ext reason node_arch node_platform base_url node_root
198
217
  local tmp_dir shasums_path archive_name ext_re install_dir archive_path
199
- local expected_hash actual_hash archive_win dest_win candidate_path next_link
218
+ local expected_hash actual_hash archive_win dest_win candidate_path next_link old_current
200
219
 
201
220
  NODE_MIN_MAJOR=20
202
221
  NODE_MIN_MINOR=9
@@ -361,19 +380,57 @@ install_private_node() {
361
380
  return 1
362
381
  fi
363
382
 
364
- next_link="$node_root/current.next.$$"
365
- if ln -s "$install_dir" "$next_link" 2>/dev/null; then
366
- if mv -Tf "$next_link" "$node_root/current" 2>/dev/null; then
367
- :
368
- elif mv -hf "$next_link" "$node_root/current" 2>/dev/null; then
369
- :
383
+ if claude_smart_is_windows; then
384
+ # Windows cannot rely on the POSIX symlink swap path; keep current recoverable.
385
+ old_current="$node_root/current.old.$$"
386
+ rm -rf "$old_current" 2>/dev/null || true
387
+ if [ -e "$node_root/current" ] || [ -L "$node_root/current" ]; then
388
+ if ! mv "$node_root/current" "$old_current"; then
389
+ rm -rf "$tmp_dir" "$install_dir"
390
+ reason="could not stage existing private Node.js install for replacement"
391
+ echo "[claude-smart] WARNING: $reason" >&2
392
+ claude_smart_write_dashboard_unavailable "$reason"
393
+ return 1
394
+ fi
395
+ fi
396
+ if mv "$install_dir" "$node_root/current"; then
397
+ rm -rf "$old_current" 2>/dev/null || true
370
398
  else
371
- rm -rf "$node_root/current"
372
- mv "$next_link" "$node_root/current"
399
+ reason="could not activate private Node.js install at $node_root/current"
400
+ if [ -e "$node_root/current" ] || [ -L "$node_root/current" ]; then
401
+ if ! rm -rf "$node_root/current"; then
402
+ reason="could not remove failed private Node.js install at $node_root/current; previous install remains at $old_current for manual recovery"
403
+ rm -rf "$tmp_dir" "$install_dir"
404
+ echo "[claude-smart] WARNING: $reason" >&2
405
+ claude_smart_write_dashboard_unavailable "$reason"
406
+ return 1
407
+ fi
408
+ fi
409
+ if [ -e "$old_current" ] || [ -L "$old_current" ]; then
410
+ if ! mv "$old_current" "$node_root/current"; then
411
+ reason="could not restore previous private Node.js install from $old_current after activation failure; previous install remains at $old_current for manual recovery"
412
+ fi
413
+ fi
414
+ rm -rf "$tmp_dir" "$install_dir"
415
+ echo "[claude-smart] WARNING: $reason" >&2
416
+ claude_smart_write_dashboard_unavailable "$reason"
417
+ return 1
373
418
  fi
374
419
  else
375
- rm -rf "$next_link" "$node_root/current"
376
- mv "$install_dir" "$node_root/current"
420
+ next_link="$node_root/current.next.$$"
421
+ if ln -s "$install_dir" "$next_link" 2>/dev/null; then
422
+ if mv -Tf "$next_link" "$node_root/current" 2>/dev/null; then
423
+ :
424
+ elif mv -hf "$next_link" "$node_root/current" 2>/dev/null; then
425
+ :
426
+ else
427
+ rm -rf "$node_root/current"
428
+ mv "$next_link" "$node_root/current"
429
+ fi
430
+ else
431
+ rm -rf "$next_link" "$node_root/current"
432
+ mv "$install_dir" "$node_root/current"
433
+ fi
377
434
  fi
378
435
 
379
436
  rm -rf "$tmp_dir"
@@ -399,6 +456,7 @@ fi
399
456
  preflight_supported_runtime_platform
400
457
 
401
458
  if install_complete; then
459
+ verify_windows_local_embedding_runtime
402
460
  start_backend_service
403
461
  claude_smart_emit_continue
404
462
  exit 0
@@ -489,7 +547,7 @@ fi
489
547
  # claude-smart's backend loads ~/.claude-smart/.env (backend-service.sh exports
490
548
  # REFLEXIO_ENV_FILE so reflexio's CLI reads it instead of ~/.reflexio/.env);
491
549
  # append our opt-in flags there so `reflexio services start` picks them up. Keep
492
- # this path in sync with env_config.REFLEXIO_ENV_PATH and backend-service.sh.
550
+ # this path in sync with env_config.CLAUDE_SMART_ENV_PATH and backend-service.sh.
493
551
  REFLEXIO_ENV="$HOME/.claude-smart/.env"
494
552
  mkdir -p "$(dirname "$REFLEXIO_ENV")"
495
553
  claude_smart_env_quote() {
@@ -562,6 +620,9 @@ claude_smart_prune_managed_env_keys_for_local() {
562
620
  }
563
621
 
564
622
  claude_smart_ensure_local_env_defaults() {
623
+ local local_cli_default local_embedding_default
624
+ local_cli_default="${CLAUDE_SMART_USE_LOCAL_CLI:-1}"
625
+ local_embedding_default="${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}"
565
626
  [ -z "${REFLEXIO_API_KEY:-}" ] || return 0
566
627
  mkdir -p "$(dirname "$REFLEXIO_ENV")"
567
628
  touch "$REFLEXIO_ENV"
@@ -569,23 +630,29 @@ claude_smart_ensure_local_env_defaults() {
569
630
  claude_smart_prune_managed_env_keys_for_local
570
631
  unset REFLEXIO_URL REFLEXIO_API_KEY REFLEXIO_USER_ID CLAUDE_SMART_MANAGED_SETUP
571
632
  if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
572
- printf '# Route reflexio generation through the local Claude Code CLI\n' >> "$REFLEXIO_ENV"
573
- claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_CLI "1"
574
- echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
633
+ printf '# Route reflexio generation through the configured local host CLI\n' >> "$REFLEXIO_ENV"
634
+ claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_CLI "$local_cli_default"
635
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=$local_cli_default to $REFLEXIO_ENV" >&2
575
636
  fi
576
637
  if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
577
638
  printf '# Use the in-process ONNX embedder (chromadb) - no API key for semantic search\n' >> "$REFLEXIO_ENV"
578
- claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_EMBEDDING "1"
579
- echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
639
+ claude_smart_env_append_raw_if_missing CLAUDE_SMART_USE_LOCAL_EMBEDDING "$local_embedding_default"
640
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=$local_embedding_default to $REFLEXIO_ENV" >&2
580
641
  fi
581
642
  if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_READ_ONLY=' "$REFLEXIO_ENV"; then
582
643
  claude_smart_env_upsert CLAUDE_SMART_READ_ONLY "0"
583
644
  echo "[claude-smart] appended CLAUDE_SMART_READ_ONLY=0 to $REFLEXIO_ENV" >&2
584
645
  fi
646
+ if ! grep -qE '^(export[[:space:]]+)?CLAUDE_SMART_HOST=' "$REFLEXIO_ENV"; then
647
+ claude_smart_env_upsert CLAUDE_SMART_HOST "claude-code"
648
+ echo "[claude-smart] appended CLAUDE_SMART_HOST=claude-code to $REFLEXIO_ENV" >&2
649
+ fi
585
650
  chmod 600 "$REFLEXIO_ENV"
586
651
  }
587
652
 
588
653
  claude_smart_ensure_local_env_defaults
654
+ # No-op outside Git Bash/MSYS/Cygwin; non-Windows installs must not depend on it.
655
+ verify_windows_local_embedding_runtime
589
656
 
590
657
  # Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
591
658
  # plugin backend port (8071). Matches the quoted and unquoted forms but
@@ -599,9 +666,23 @@ if [ -f "$REFLEXIO_ENV" ] && grep -qE '^REFLEXIO_URL=("http://localhost:8081/?"|
599
666
  echo "[claude-smart] migrated REFLEXIO_URL 8081 → 8071 in $REFLEXIO_ENV (backup at $REFLEXIO_ENV.bak)" >&2
600
667
  fi
601
668
 
602
- if ! command -v claude >/dev/null 2>&1; then
603
- echo "[claude-smart] WARNING: 'claude' CLI not on PATH — reflexio extractors will have no LLM until it's installed" >&2
604
- fi
669
+ case "${CLAUDE_SMART_HOST:-claude-code}" in
670
+ opencode)
671
+ if [ -z "${CLAUDE_SMART_OPENCODE_PATH:-}" ] && ! command -v opencode >/dev/null 2>&1; then
672
+ echo "[claude-smart] WARNING: 'opencode' CLI not on PATH and CLAUDE_SMART_OPENCODE_PATH is not set — reflexio extractors will have no LLM until OpenCode is installed" >&2
673
+ fi
674
+ ;;
675
+ codex)
676
+ if ! command -v codex >/dev/null 2>&1; then
677
+ echo "[claude-smart] WARNING: 'codex' CLI not on PATH — reflexio extractors will have no LLM until Codex is installed" >&2
678
+ fi
679
+ ;;
680
+ *)
681
+ if ! command -v claude >/dev/null 2>&1; then
682
+ echo "[claude-smart] WARNING: 'claude' CLI not on PATH — reflexio extractors will have no LLM until Claude Code is installed" >&2
683
+ fi
684
+ ;;
685
+ esac
605
686
 
606
687
  LEGACY_CS_CITE="$HOME/.claude-smart/bin/cs-cite"
607
688
  if [ -e "$LEGACY_CS_CITE" ]; then
@@ -28,7 +28,7 @@ Description: Python package powering the claude-smart plugin — hook handlers t
28
28
 
29
29
  | File | Purpose |
30
30
  |------|---------|
31
- | `env_config.py` | Parses `~/.claude-smart/.env` (`REFLEXIO_URL`, `REFLEXIO_API_KEY`, `CLAUDE_SMART_READ_ONLY`, local embedding/CLI flags). |
31
+ | `env_config.py` | Parses `~/.claude-smart/.env` (`REFLEXIO_URL`, `REFLEXIO_API_KEY`, `CLAUDE_SMART_READ_ONLY`, `CLAUDE_SMART_HOST`, local embedding/CLI flags). |
32
32
  | `runtime.py` | Host detection (Claude Code vs Codex); shared agent version. |
33
33
  | `ids.py` | Session / project ID generation and resolution. |
34
34
  | `context_inject.py`, `context_format.py`, `query_compose.py`, `cs_cite.py` | Build search queries, format learned skills as markdown, inject into context, format citations. |
@@ -37,10 +37,14 @@ from pathlib import Path
37
37
  from urllib.parse import urlparse
38
38
  from urllib.request import url2pathname
39
39
 
40
- from claude_smart import context_format, cs_cite, env_config, ids, publish, state
40
+ from claude_smart import context_format, cs_cite, env_config, ids, publish, runtime, state
41
41
  from claude_smart.reflexio_adapter import Adapter
42
42
 
43
- _REFLEXIO_ENV_PATH = env_config.REFLEXIO_ENV_PATH
43
+ _HOST_CLAUDE_CODE = runtime.HOST_CLAUDE_CODE
44
+ _HOST_CODEX = runtime.HOST_CODEX
45
+ _HOST_OPENCODE = runtime.HOST_OPENCODE
46
+ _HOST_CHOICES = (_HOST_CLAUDE_CODE, _HOST_CODEX, _HOST_OPENCODE)
47
+ _CLAUDE_SMART_ENV_PATH = env_config.CLAUDE_SMART_ENV_PATH
44
48
  _MANAGED_REFLEXIO_URL = env_config.MANAGED_REFLEXIO_URL
45
49
  _PLUGIN_SPEC = "claude-smart@reflexioai"
46
50
  _CODEX_MARKETPLACE_NAME = "reflexioai"
@@ -99,6 +103,7 @@ _DEFAULT_STORAGE_ROOT = _REFLEXIO_DIR / "data"
99
103
  _REFLEXIO_ORG_ID = "claude-smart"
100
104
  _REFLEXIO_CONFIG_PATH = _REFLEXIO_DIR / "configs" / f"config_{_REFLEXIO_ORG_ID}.json"
101
105
  _LOCAL_STORAGE_ENV = "LOCAL_STORAGE_PATH"
106
+ _OPENCODE_PATH_ENV = "CLAUDE_SMART_OPENCODE_PATH"
102
107
  _CODEX_REQUIRED_FILES = (
103
108
  Path(".agents/plugins/marketplace.json"),
104
109
  Path("plugin/.codex-plugin/plugin.json"),
@@ -927,15 +932,20 @@ def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
927
932
  return False, output or "Codex CLI does not expose plugin marketplace commands"
928
933
 
929
934
 
930
- def _configure_reflexio_setup() -> bool:
935
+ def _configure_reflexio_setup(host: str = _HOST_CLAUDE_CODE) -> bool:
931
936
  """Load setup state and ensure local defaults when unmanaged.
932
937
 
938
+ Args:
939
+ host: Host integration being installed. Persisted in local mode so
940
+ later backend restarts can wire the same extraction bridge even
941
+ when the host plugin server is not the process launcher.
942
+
933
943
  Returns:
934
944
  bool: Whether read-only mode is enabled.
935
945
  """
936
- env_config.load_reflexio_env(_REFLEXIO_ENV_PATH)
946
+ env_config.load_reflexio_env(_CLAUDE_SMART_ENV_PATH)
937
947
  try:
938
- env_text = _REFLEXIO_ENV_PATH.read_text()
948
+ env_text = _CLAUDE_SMART_ENV_PATH.read_text()
939
949
  except OSError:
940
950
  env_text = ""
941
951
  read_only_value = ""
@@ -975,9 +985,9 @@ def _configure_reflexio_setup() -> bool:
975
985
  os.environ.pop(env_config.REFLEXIO_API_KEY_ENV, None)
976
986
  os.environ.pop("REFLEXIO_USER_ID", None)
977
987
  os.environ.pop("CLAUDE_SMART_MANAGED_SETUP", None)
978
- added = env_config.ensure_local_env_defaults(_REFLEXIO_ENV_PATH)
988
+ added = env_config.ensure_local_env_defaults(_CLAUDE_SMART_ENV_PATH, host=host)
979
989
  if added:
980
- sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
990
+ sys.stdout.write(f"Seeded {_CLAUDE_SMART_ENV_PATH} with {', '.join(added)}.\n")
981
991
  return read_only
982
992
 
983
993
 
@@ -1174,7 +1184,7 @@ def _patch_opencode_plugin_config(
1174
1184
 
1175
1185
 
1176
1186
  def _has_extraction_provider() -> bool:
1177
- env_config.load_reflexio_env(_REFLEXIO_ENV_PATH)
1187
+ env_config.load_reflexio_env(_CLAUDE_SMART_ENV_PATH)
1178
1188
  if os.environ.get(env_config.REFLEXIO_API_KEY_ENV, "").strip():
1179
1189
  return True
1180
1190
  cli_path = os.environ.get("CLAUDE_SMART_CLI_PATH", "").strip()
@@ -1182,7 +1192,7 @@ def _has_extraction_provider() -> bool:
1182
1192
  resolved = Path(cli_path).expanduser()
1183
1193
  if resolved.is_file() and os.access(resolved, os.X_OK):
1184
1194
  return True
1185
- return bool(shutil.which("claude") or shutil.which("codex") or shutil.which("opencode"))
1195
+ return bool(shutil.which("claude") or shutil.which("codex") or _resolve_opencode_path())
1186
1196
 
1187
1197
 
1188
1198
  def _extraction_provider_error() -> str:
@@ -1193,6 +1203,41 @@ def _extraction_provider_error() -> str:
1193
1203
  )
1194
1204
 
1195
1205
 
1206
+ def _has_opencode_cli() -> bool:
1207
+ return _resolve_opencode_path() is not None
1208
+
1209
+
1210
+ def _resolve_opencode_path() -> str | None:
1211
+ opencode_path = os.environ.get(_OPENCODE_PATH_ENV, "").strip()
1212
+ if opencode_path:
1213
+ resolved = Path(opencode_path).expanduser()
1214
+ if resolved.is_file() and os.access(resolved, os.X_OK):
1215
+ return str(resolved)
1216
+ return shutil.which("opencode")
1217
+
1218
+
1219
+ def _persist_opencode_path() -> list[str]:
1220
+ resolved = _resolve_opencode_path()
1221
+ if not resolved:
1222
+ return []
1223
+ os.environ[_OPENCODE_PATH_ENV] = resolved
1224
+ return env_config.set_env_vars(_CLAUDE_SMART_ENV_PATH, {_OPENCODE_PATH_ENV: resolved})
1225
+
1226
+
1227
+ def _opencode_prerequisite_error() -> str | None:
1228
+ if not _has_opencode_cli():
1229
+ return (
1230
+ "error: OpenCode CLI not found on PATH. Install OpenCode first, "
1231
+ "or set CLAUDE_SMART_OPENCODE_PATH to the OpenCode executable.\n"
1232
+ )
1233
+ if os.name == "nt" and not _resolve_bash():
1234
+ return (
1235
+ "error: Git Bash is required for claude-smart OpenCode support on Windows. "
1236
+ "Install Git for Windows and ensure bash.exe is on PATH, or run OpenCode from WSL.\n"
1237
+ )
1238
+ return None
1239
+
1240
+
1196
1241
  def _opencode_install_supported_from_this_package() -> bool:
1197
1242
  return (_SCRIPTS_DIR / "smart-install.sh").is_file()
1198
1243
 
@@ -1332,7 +1377,12 @@ def cmd_install_opencode(args: argparse.Namespace) -> int:
1332
1377
  "Run `npx claude-smart install --host opencode`.\n"
1333
1378
  )
1334
1379
  return 1
1335
- read_only = _configure_reflexio_setup()
1380
+ prerequisite_error = _opencode_prerequisite_error()
1381
+ if prerequisite_error:
1382
+ sys.stderr.write(prerequisite_error)
1383
+ return 1
1384
+ read_only = _configure_reflexio_setup(host=_HOST_OPENCODE)
1385
+ _persist_opencode_path()
1336
1386
  if not _has_extraction_provider():
1337
1387
  sys.stderr.write(_extraction_provider_error())
1338
1388
  return 1
@@ -1387,7 +1437,7 @@ def cmd_install_codex(args: argparse.Namespace) -> int:
1387
1437
  "error: 'uv' not found on PATH. Install uv or restart your shell.\n"
1388
1438
  )
1389
1439
  return 1
1390
- read_only = _configure_reflexio_setup()
1440
+ read_only = _configure_reflexio_setup(host=_HOST_CODEX)
1391
1441
 
1392
1442
  missing = _missing_codex_marketplace_files(_REPO_ROOT)
1393
1443
  if missing:
@@ -1485,9 +1535,9 @@ def cmd_install(args: argparse.Namespace) -> int:
1485
1535
  Returns:
1486
1536
  int: 0 on success, non-zero if the ``claude`` CLI is missing or fails.
1487
1537
  """
1488
- if getattr(args, "host", "claude-code") == "codex":
1538
+ if getattr(args, "host", _HOST_CLAUDE_CODE) == _HOST_CODEX:
1489
1539
  return cmd_install_codex(args)
1490
- if getattr(args, "host", "claude-code") == "opencode":
1540
+ if getattr(args, "host", _HOST_CLAUDE_CODE) == _HOST_OPENCODE:
1491
1541
  return cmd_install_opencode(args)
1492
1542
 
1493
1543
  if not shutil.which("claude"):
@@ -1496,7 +1546,7 @@ def cmd_install(args: argparse.Namespace) -> int:
1496
1546
  "Install Claude Code first: https://claude.com/claude-code\n"
1497
1547
  )
1498
1548
  return 1
1499
- read_only = _configure_reflexio_setup()
1549
+ read_only = _configure_reflexio_setup(host=_HOST_CLAUDE_CODE)
1500
1550
 
1501
1551
  refresh_existing = bool(getattr(args, "refresh_existing", False))
1502
1552
  for cmd in (
@@ -1559,16 +1609,16 @@ def cmd_update(args: argparse.Namespace) -> int:
1559
1609
  Returns:
1560
1610
  int: 0 on success, non-zero if the host CLI is missing or install fails.
1561
1611
  """
1562
- if getattr(args, "host", "claude-code") == "codex":
1612
+ if getattr(args, "host", _HOST_CLAUDE_CODE) == _HOST_CODEX:
1563
1613
  return cmd_update_codex(args)
1564
- if getattr(args, "host", "claude-code") == "opencode":
1614
+ if getattr(args, "host", _HOST_CLAUDE_CODE) == _HOST_OPENCODE:
1565
1615
  return cmd_update_opencode(args)
1566
1616
 
1567
1617
  _run_service(_DASHBOARD_SCRIPT, "stop")
1568
1618
  _run_service(_BACKEND_SCRIPT, "stop")
1569
1619
  sys.stdout.write("Updating claude-smart by reinstalling from this package...\n")
1570
1620
  install_args = argparse.Namespace(**vars(args), refresh_existing=True)
1571
- install_args.host = "claude-code"
1621
+ install_args.host = _HOST_CLAUDE_CODE
1572
1622
  return cmd_install(install_args)
1573
1623
 
1574
1624
 
@@ -1585,7 +1635,7 @@ def cmd_update_codex(_args: argparse.Namespace) -> int:
1585
1635
  "Updating claude-smart Codex support by reinstalling from this package...\n"
1586
1636
  )
1587
1637
  install_args = argparse.Namespace(**vars(_args))
1588
- install_args.host = "codex"
1638
+ install_args.host = _HOST_CODEX
1589
1639
  return cmd_install_codex(install_args)
1590
1640
 
1591
1641
 
@@ -1596,7 +1646,7 @@ def cmd_update_opencode(args: argparse.Namespace) -> int:
1596
1646
  "Updating claude-smart OpenCode support by reinstalling from this package...\n"
1597
1647
  )
1598
1648
  install_args = argparse.Namespace(**vars(args))
1599
- install_args.host = "opencode"
1649
+ install_args.host = _HOST_OPENCODE
1600
1650
  return cmd_install_opencode(install_args)
1601
1651
 
1602
1652
 
@@ -1612,9 +1662,9 @@ def cmd_uninstall(_args: argparse.Namespace) -> int:
1612
1662
  Returns:
1613
1663
  int: 0 on success, non-zero if the ``claude`` CLI is missing or fails.
1614
1664
  """
1615
- if getattr(_args, "host", "claude-code") == "codex":
1665
+ if getattr(_args, "host", _HOST_CLAUDE_CODE) == _HOST_CODEX:
1616
1666
  return cmd_uninstall_codex(_args)
1617
- if getattr(_args, "host", "claude-code") == "opencode":
1667
+ if getattr(_args, "host", _HOST_CLAUDE_CODE) == _HOST_OPENCODE:
1618
1668
  return cmd_uninstall_opencode(_args)
1619
1669
 
1620
1670
  if not shutil.which("claude"):
@@ -1761,7 +1811,7 @@ def _reflexio_show_footer() -> str:
1761
1811
  str: A leading-separator markdown footer ending in a newline.
1762
1812
  """
1763
1813
  return (
1764
- f"\n---\n⭐ claude-smart is powered by [reflexio]({cs_cite.REFLEXIO_REPO_URL})"
1814
+ f"\n---\nclaude-smart is powered by [reflexio]({cs_cite.REFLEXIO_REPO_URL})"
1765
1815
  f" — if it helps you, [star it on GitHub]({cs_cite.REFLEXIO_REPO_URL}).\n"
1766
1816
  )
1767
1817
 
@@ -1949,7 +1999,7 @@ def _resolve_absolute_path(raw_path: str | Path, *, source: str) -> Path:
1949
1999
  def _effective_storage_root() -> Path:
1950
2000
  raw = os.environ.get(_LOCAL_STORAGE_ENV, "").strip()
1951
2001
  if not raw:
1952
- raw = _read_dotenv_value(_REFLEXIO_ENV_PATH, _LOCAL_STORAGE_ENV) or ""
2002
+ raw = _read_dotenv_value(_CLAUDE_SMART_ENV_PATH, _LOCAL_STORAGE_ENV) or ""
1953
2003
  return _resolve_absolute_path(
1954
2004
  raw or _DEFAULT_STORAGE_ROOT, source=_LOCAL_STORAGE_ENV
1955
2005
  )
@@ -2449,8 +2499,8 @@ def _build_parser() -> argparse.ArgumentParser:
2449
2499
  inst = sub.add_parser("install", help="Install claude-smart")
2450
2500
  inst.add_argument(
2451
2501
  "--host",
2452
- choices=("claude-code", "codex", "opencode"),
2453
- default="claude-code",
2502
+ choices=_HOST_CHOICES,
2503
+ default=_HOST_CLAUDE_CODE,
2454
2504
  help="Install target host",
2455
2505
  )
2456
2506
  inst.add_argument(
@@ -2464,8 +2514,8 @@ def _build_parser() -> argparse.ArgumentParser:
2464
2514
  upd = sub.add_parser("update", help="Update claude-smart to the latest version")
2465
2515
  upd.add_argument(
2466
2516
  "--host",
2467
- choices=("claude-code", "codex", "opencode"),
2468
- default="claude-code",
2517
+ choices=_HOST_CHOICES,
2518
+ default=_HOST_CLAUDE_CODE,
2469
2519
  help="Update target host",
2470
2520
  )
2471
2521
  upd.add_argument(
@@ -2479,8 +2529,8 @@ def _build_parser() -> argparse.ArgumentParser:
2479
2529
  uni = sub.add_parser("uninstall", help="Remove claude-smart")
2480
2530
  uni.add_argument(
2481
2531
  "--host",
2482
- choices=("claude-code", "codex", "opencode"),
2483
- default="claude-code",
2532
+ choices=_HOST_CHOICES,
2533
+ default=_HOST_CLAUDE_CODE,
2484
2534
  help="Uninstall target host",
2485
2535
  )
2486
2536
  uni.add_argument(
@@ -13,17 +13,23 @@ from __future__ import annotations
13
13
  import os
14
14
  from pathlib import Path
15
15
 
16
- REFLEXIO_ENV_PATH = Path.home() / ".claude-smart" / ".env"
16
+ from claude_smart import runtime
17
+
18
+ CLAUDE_SMART_ENV_PATH = Path.home() / ".claude-smart" / ".env"
19
+ # Deprecated internal alias kept so older imports do not fail abruptly.
20
+ REFLEXIO_ENV_PATH = CLAUDE_SMART_ENV_PATH
17
21
  MANAGED_REFLEXIO_URL = "https://www.reflexio.ai/"
18
22
  REFLEXIO_URL_ENV = "REFLEXIO_URL"
19
23
  REFLEXIO_API_KEY_ENV = "REFLEXIO_API_KEY"
20
24
  CLAUDE_SMART_READ_ONLY_ENV = "CLAUDE_SMART_READ_ONLY"
21
25
  CLAUDE_SMART_USE_LOCAL_CLI_ENV = "CLAUDE_SMART_USE_LOCAL_CLI"
22
26
  CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV = "CLAUDE_SMART_USE_LOCAL_EMBEDDING"
27
+ CLAUDE_SMART_HOST_ENV = "CLAUDE_SMART_HOST"
28
+ DEFAULT_CLAUDE_SMART_HOST = runtime.HOST_CLAUDE_CODE
23
29
 
24
30
  _LOCAL_DEFAULT_ENTRIES = (
25
31
  (
26
- "# Route reflexio generation through the local Claude Code CLI",
32
+ "# Route reflexio generation through the configured local host CLI",
27
33
  CLAUDE_SMART_USE_LOCAL_CLI_ENV,
28
34
  "1",
29
35
  ),
@@ -33,7 +39,12 @@ _LOCAL_DEFAULT_ENTRIES = (
33
39
  "1",
34
40
  ),
35
41
  (None, CLAUDE_SMART_READ_ONLY_ENV, "0"),
42
+ (None, CLAUDE_SMART_HOST_ENV, DEFAULT_CLAUDE_SMART_HOST),
36
43
  )
44
+ _ENV_OVERRIDABLE_LOCAL_DEFAULT_KEYS = {
45
+ CLAUDE_SMART_USE_LOCAL_CLI_ENV,
46
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING_ENV,
47
+ }
37
48
  _LOCAL_MODE_PRUNE_KEYS = {
38
49
  REFLEXIO_URL_ENV,
39
50
  REFLEXIO_API_KEY_ENV,
@@ -69,7 +80,7 @@ def parse_env_line(line: str) -> tuple[str, str] | None:
69
80
 
70
81
  def load_reflexio_env(path: Path | None = None) -> None:
71
82
  """Load ``~/.claude-smart/.env`` into ``os.environ`` without overriding values."""
72
- path = path or REFLEXIO_ENV_PATH
83
+ path = path or CLAUDE_SMART_ENV_PATH
73
84
  try:
74
85
  text = path.read_text()
75
86
  except OSError:
@@ -118,14 +129,19 @@ def set_env_vars(path: Path, values: dict[str, str]) -> list[str]:
118
129
  return added
119
130
 
120
131
 
121
- def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
132
+ def ensure_local_env_defaults(
133
+ path: Path | None = None,
134
+ host: str = DEFAULT_CLAUDE_SMART_HOST,
135
+ ) -> list[str]:
122
136
  """Create or augment ``~/.claude-smart/.env`` for claude-smart local mode.
123
137
 
124
- Existing active assignments win. This repairs first installs and deleted
125
- env files without clobbering explicit user overrides such as
126
- ``CLAUDE_SMART_READ_ONLY=1``.
138
+ Existing active assignments win except ``CLAUDE_SMART_HOST``, which is
139
+ install-selected state and must follow the host currently being installed.
140
+ This repairs first installs and deleted env files without clobbering
141
+ explicit user overrides such as ``CLAUDE_SMART_READ_ONLY=1``.
127
142
  """
128
- path = path or REFLEXIO_ENV_PATH
143
+ install_host = host
144
+ path = path or CLAUDE_SMART_ENV_PATH
129
145
  path.parent.mkdir(parents=True, exist_ok=True)
130
146
  try:
131
147
  existing = path.read_text()
@@ -135,6 +151,8 @@ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
135
151
  present: set[str] = set()
136
152
  kept_lines: list[str] = []
137
153
  pruned = False
154
+ changed = False
155
+ host_written = False
138
156
  for line in existing.splitlines():
139
157
  parsed = parse_env_line(line)
140
158
  if parsed is not None:
@@ -142,6 +160,19 @@ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
142
160
  if key in _LOCAL_MODE_PRUNE_KEYS:
143
161
  pruned = True
144
162
  continue
163
+ if key == CLAUDE_SMART_HOST_ENV:
164
+ present.add(key)
165
+ if not host_written:
166
+ replacement = (
167
+ f"{CLAUDE_SMART_HOST_ENV}={_escape_env_value(install_host)}"
168
+ )
169
+ kept_lines.append(replacement)
170
+ host_written = True
171
+ if line != replacement or _value != install_host:
172
+ changed = True
173
+ else:
174
+ changed = True
175
+ continue
145
176
  present.add(key)
146
177
  kept_lines.append(line)
147
178
 
@@ -150,15 +181,16 @@ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
150
181
  for comment, key, value in _LOCAL_DEFAULT_ENTRIES:
151
182
  if key in present:
152
183
  continue
184
+ effective_value = _resolve_local_env_default(key, value, install_host)
153
185
  if comment:
154
186
  additions.append(comment)
155
187
  if key == CLAUDE_SMART_READ_ONLY_ENV:
156
- additions.append(f'{key}="{_escape_env_value(value)}"')
188
+ additions.append(f'{key}="{_escape_env_value(effective_value)}"')
157
189
  else:
158
- additions.append(f"{key}={_escape_env_value(value)}")
190
+ additions.append(f"{key}={_escape_env_value(effective_value)}")
159
191
  added_keys.append(key)
160
192
 
161
- if additions or pruned:
193
+ if additions or pruned or changed:
162
194
  content = "\n".join(kept_lines)
163
195
  if additions:
164
196
  prefix = "" if not content or content.endswith("\n") else "\n"
@@ -171,6 +203,16 @@ def ensure_local_env_defaults(path: Path | None = None) -> list[str]:
171
203
  return added_keys
172
204
 
173
205
 
206
+ def _resolve_local_env_default(key: str, fallback: str, install_host: str) -> str:
207
+ if key == CLAUDE_SMART_HOST_ENV:
208
+ return install_host
209
+ if key in _ENV_OVERRIDABLE_LOCAL_DEFAULT_KEYS:
210
+ explicit = os.environ.get(key, "").strip()
211
+ if explicit:
212
+ return explicit
213
+ return fallback
214
+
215
+
174
216
  def env_truthy(name: str) -> bool:
175
217
  """Return True when an environment flag is explicitly enabled."""
176
218
  return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
package/plugin/uv.lock CHANGED
@@ -476,7 +476,7 @@ wheels = [
476
476
 
477
477
  [[package]]
478
478
  name = "claude-smart"
479
- version = "0.2.48"
479
+ version = "0.2.49"
480
480
  source = { editable = "." }
481
481
  dependencies = [
482
482
  { name = "chromadb" },
@@ -494,7 +494,7 @@ dev = [
494
494
  requires-dist = [
495
495
  { name = "chromadb", specifier = ">=0.5" },
496
496
  { name = "einops", specifier = ">=0.8.0" },
497
- { name = "reflexio-ai", specifier = ">=0.2.27" },
497
+ { name = "reflexio-ai", specifier = ">=0.2.28" },
498
498
  { name = "sqlite-vec", specifier = ">=0.1.6" },
499
499
  ]
500
500
 
@@ -2760,7 +2760,7 @@ wheels = [
2760
2760
 
2761
2761
  [[package]]
2762
2762
  name = "reflexio-ai"
2763
- version = "0.2.27"
2763
+ version = "0.2.28"
2764
2764
  source = { registry = "https://pypi.org/simple" }
2765
2765
  dependencies = [
2766
2766
  { name = "aiohttp" },
@@ -2800,9 +2800,9 @@ dependencies = [
2800
2800
  { name = "websocket-client" },
2801
2801
  { name = "xlsxwriter" },
2802
2802
  ]
2803
- sdist = { url = "https://files.pythonhosted.org/packages/85/22/64429eb84ae74aaa6d36f0a2e0d91a27b35ee33ef17c18411d54f08706ba/reflexio_ai-0.2.27.tar.gz", hash = "sha256:e433209ca848245e957943c6476f2bba32581de8804d535d25ca0596b0da0afd", size = 1112139, upload-time = "2026-06-30T06:26:21.88Z" }
2803
+ sdist = { url = "https://files.pythonhosted.org/packages/2a/e6/a59fc40136b37ea3e9b6878b600ef1f405c1baf7d1431a07e55c8e49c14e/reflexio_ai-0.2.28.tar.gz", hash = "sha256:49a35abf714ae293392277b278b895cde21ca50c14f10c97cbcf27c307e49fae", size = 1155554, upload-time = "2026-07-04T07:01:02.207Z" }
2804
2804
  wheels = [
2805
- { url = "https://files.pythonhosted.org/packages/68/22/d65332c75b9072e8838d51f68354d8d17c7636aa1cf73a20cfc1147b4364/reflexio_ai-0.2.27-py3-none-any.whl", hash = "sha256:4598f4aaa61e3c755a04573ab9711a7b5673ebf268029b2cef21cdcd5d4abfcf", size = 1461400, upload-time = "2026-06-30T06:26:19.965Z" },
2805
+ { url = "https://files.pythonhosted.org/packages/96/a6/c842d5e525f0f561fcd11ecf6543c9c52b3f4b665dbc2a8fe94adce9be1b/reflexio_ai-0.2.28-py3-none-any.whl", hash = "sha256:177e3a740afa2acb5a2a14a1e648c32c2d9e71ef9ff79b8ab2caec7e24ff5d88", size = 1558983, upload-time = "2026-07-04T07:01:00.69Z" },
2806
2806
  ]
2807
2807
 
2808
2808
  [[package]]
@@ -78,6 +78,9 @@ When `REFLEXIO_EMBEDDING_PROVIDER=local_service` or
78
78
  `local/nomic-embed-text-v1.5`, and `local/minilm-l6-v2`. Local embedding
79
79
  requests allow extra time for model cold start; override with
80
80
  `REFLEXIO_EMBEDDING_SERVICE_TIMEOUT_MS` when needed.
81
+ MiniLM cache corruption is retried once automatically. If recovery still fails,
82
+ delete the cache directory named in the error message, restart Reflexio, and
83
+ retry local embedding.
81
84
 
82
85
  ## Publishing interactions
83
86