claude-smart 0.2.30 → 0.2.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +2 -2
  2. package/bin/claude-smart.js +172 -18
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/dashboard/app/api/config/route.ts +11 -2
  7. package/plugin/dashboard/app/api/health/route.ts +45 -6
  8. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +15 -7
  9. package/plugin/dashboard/app/api/rules/applied/route.ts +27 -0
  10. package/plugin/dashboard/app/configure/env/page.tsx +36 -31
  11. package/plugin/dashboard/app/configure/layout.tsx +1 -1
  12. package/plugin/dashboard/app/configure/server/page.tsx +8 -14
  13. package/plugin/dashboard/app/dashboard/page.tsx +311 -115
  14. package/plugin/dashboard/app/globals.css +80 -66
  15. package/plugin/dashboard/app/layout.tsx +13 -10
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +21 -32
  17. package/plugin/dashboard/app/preferences/page.tsx +154 -54
  18. package/plugin/dashboard/app/preferences/project/[id]/page.tsx +1 -0
  19. package/plugin/dashboard/app/rules/[id]/page.tsx +51 -0
  20. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +4 -4
  21. package/plugin/dashboard/app/sessions/page.tsx +14 -10
  22. package/plugin/dashboard/app/skills/page.tsx +175 -56
  23. package/plugin/dashboard/app/skills/project/[id]/page.tsx +22 -38
  24. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +20 -37
  25. package/plugin/dashboard/components/common/delete-learning-danger-zone.tsx +166 -0
  26. package/plugin/dashboard/components/common/empty-state.tsx +4 -2
  27. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  28. package/plugin/dashboard/components/common/page-header.tsx +5 -3
  29. package/plugin/dashboard/components/common/page-tabs.tsx +4 -4
  30. package/plugin/dashboard/components/common/stat-card.tsx +9 -5
  31. package/plugin/dashboard/components/layout/sidebar.tsx +24 -9
  32. package/plugin/dashboard/components/layout/top-bar.tsx +37 -25
  33. package/plugin/dashboard/components/ui/input.tsx +1 -0
  34. package/plugin/dashboard/hooks/use-settings.tsx +30 -61
  35. package/plugin/dashboard/hooks/use-stall-state.ts +5 -9
  36. package/plugin/dashboard/lib/config-file.ts +2 -0
  37. package/plugin/dashboard/lib/reflexio-client.ts +23 -48
  38. package/plugin/dashboard/lib/session-reader.ts +222 -6
  39. package/plugin/dashboard/lib/types.ts +20 -1
  40. package/plugin/dashboard/package-lock.json +70 -95
  41. package/plugin/dashboard/package.json +5 -2
  42. package/plugin/hooks/hooks.json +1 -1
  43. package/plugin/pyproject.toml +1 -1
  44. package/plugin/scripts/_lib.sh +126 -0
  45. package/plugin/scripts/backend-service.sh +32 -7
  46. package/plugin/scripts/cli.sh +4 -2
  47. package/plugin/scripts/codex-hook.js +100 -3
  48. package/plugin/scripts/dashboard-service.sh +98 -19
  49. package/plugin/scripts/hook_entry.sh +32 -11
  50. package/plugin/scripts/smart-install.sh +27 -44
  51. package/plugin/src/claude_smart/cli.py +204 -20
  52. package/plugin/src/claude_smart/context_format.py +244 -6
  53. package/plugin/src/claude_smart/context_inject.py +8 -1
  54. package/plugin/src/claude_smart/cs_cite.py +186 -34
  55. package/plugin/src/claude_smart/env_config.py +102 -0
  56. package/plugin/src/claude_smart/events/session_end.py +171 -6
  57. package/plugin/src/claude_smart/events/session_start.py +26 -2
  58. package/plugin/src/claude_smart/events/stop.py +48 -9
  59. package/plugin/src/claude_smart/hook.py +62 -4
  60. package/plugin/src/claude_smart/hook_log.py +301 -0
  61. package/plugin/src/claude_smart/internal_call.py +30 -0
  62. package/plugin/src/claude_smart/publish.py +5 -0
  63. package/plugin/src/claude_smart/reflexio_adapter.py +102 -26
  64. package/plugin/uv.lock +1 -1
@@ -14,6 +14,7 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
14
14
  claude_smart_source_login_path
15
15
  claude_smart_prepend_astral_bins
16
16
  claude_smart_prepend_node_bins
17
+ claude_smart_source_reflexio_env
17
18
 
18
19
  PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
19
20
  REPO_ROOT="$(cd "$HERE/../.." && pwd)"
@@ -47,7 +48,7 @@ acquire_install_lock() {
47
48
  exec 9>"$INSTALL_LOCK"
48
49
  if ! flock 9; then
49
50
  echo "[claude-smart] install lock failed; continuing without serialization" >&2
50
- echo '{"continue":true,"suppressOutput":true}'
51
+ claude_smart_emit_continue
51
52
  exit 0
52
53
  fi
53
54
  return 0
@@ -80,45 +81,23 @@ acquire_install_lock
80
81
  rm -f "$FAILURE_MARKER"
81
82
 
82
83
  write_failure() {
83
- local reason
84
+ local reason fp_hash
84
85
  reason="$1"
85
- printf '%s\n' "$reason" > "$FAILURE_MARKER"
86
+ fp_hash="$(claude_smart_install_fingerprint_hash "$PLUGIN_ROOT" "$HERE" 2>/dev/null || true)"
87
+ {
88
+ printf '%s\n' "$reason"
89
+ if [ -n "$fp_hash" ]; then
90
+ printf 'fingerprint=%s\n' "$fp_hash"
91
+ fi
92
+ } > "$FAILURE_MARKER"
86
93
  rm -f "$SUCCESS_MARKER"
87
94
  echo "[claude-smart] install failed: $reason" >&2
88
- echo '{"continue":true,"suppressOutput":true}'
95
+ claude_smart_emit_continue
89
96
  exit 0
90
97
  }
91
98
 
92
- fingerprint_file() {
93
- local path
94
- path="$1"
95
- if [ -f "$path" ]; then
96
- cksum "$path" 2>/dev/null | awk '{print $1 ":" $2}'
97
- else
98
- printf 'missing\n'
99
- fi
100
- }
101
-
102
99
  install_fingerprint() {
103
- printf 'plugin_root=%s\n' "$PLUGIN_ROOT"
104
- printf 'smart_install=%s\n' "$(fingerprint_file "$HERE/smart-install.sh")"
105
- printf 'pyproject=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/pyproject.toml")"
106
- printf 'uv_lock=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/uv.lock")"
107
- # Resolved python interpreter — catches a system upgrade (3.12.4 → 3.12.5)
108
- # that would otherwise let install_complete return true against a venv
109
- # built against a now-deleted interpreter.
110
- if command -v uv >/dev/null 2>&1; then
111
- printf 'python=%s\n' "$(uv python find 3.12 2>/dev/null || echo missing)"
112
- else
113
- printf 'python=no-uv\n'
114
- fi
115
- if [ -d "$PLUGIN_ROOT/dashboard" ]; then
116
- printf 'dashboard_pkg=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/dashboard/package.json")"
117
- printf 'dashboard_lock=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/dashboard/package-lock.json")"
118
- else
119
- printf 'dashboard_pkg=none\n'
120
- printf 'dashboard_lock=none\n'
121
- fi
100
+ claude_smart_install_fingerprint "$PLUGIN_ROOT" "$HERE"
122
101
  }
123
102
 
124
103
  install_complete() {
@@ -127,8 +106,10 @@ install_complete() {
127
106
  command -v uv >/dev/null 2>&1 || return 1
128
107
  [ -d "$PLUGIN_ROOT/.venv" ] || return 1
129
108
  [ -f "$HOME/.reflexio/.env" ] || return 1
130
- grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.reflexio/.env" || return 1
131
- grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.reflexio/.env" || return 1
109
+ if [ -z "${REFLEXIO_API_KEY:-}" ]; then
110
+ grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.reflexio/.env" || return 1
111
+ grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.reflexio/.env" || return 1
112
+ fi
132
113
  if [ -d "$PLUGIN_ROOT/dashboard" ]; then
133
114
  [ -d "$PLUGIN_ROOT/dashboard/.next" ] || [ -f "$MARKER_DIR/dashboard-build.pid" ] || [ -f "$(claude_smart_dashboard_unavailable_marker)" ] || return 1
134
115
  fi
@@ -382,7 +363,7 @@ fi
382
363
  preflight_supported_runtime_platform
383
364
 
384
365
  if install_complete; then
385
- echo '{"continue":true,"suppressOutput":true}'
366
+ claude_smart_emit_continue
386
367
  exit 0
387
368
  fi
388
369
 
@@ -453,13 +434,15 @@ fi
453
434
  REFLEXIO_ENV="$HOME/.reflexio/.env"
454
435
  mkdir -p "$(dirname "$REFLEXIO_ENV")"
455
436
  touch "$REFLEXIO_ENV"
456
- if ! grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
457
- printf '\n# Route reflexio generation through the local Claude Code CLI\nCLAUDE_SMART_USE_LOCAL_CLI=1\n' >> "$REFLEXIO_ENV"
458
- echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
459
- fi
460
- if ! grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
461
- printf '# Use the in-process ONNX embedder (chromadb) — no API key for semantic search\nCLAUDE_SMART_USE_LOCAL_EMBEDDING=1\n' >> "$REFLEXIO_ENV"
462
- echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
437
+ if [ -z "${REFLEXIO_API_KEY:-}" ]; then
438
+ if ! grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
439
+ printf '\n# Route reflexio generation through the local Claude Code CLI\nCLAUDE_SMART_USE_LOCAL_CLI=1\n' >> "$REFLEXIO_ENV"
440
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
441
+ fi
442
+ if ! grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
443
+ printf '# Use the in-process ONNX embedder (chromadb) — no API key for semantic search\nCLAUDE_SMART_USE_LOCAL_EMBEDDING=1\n' >> "$REFLEXIO_ENV"
444
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
445
+ fi
463
446
  fi
464
447
  # Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
465
448
  # plugin backend port (8071). Matches the quoted and unquoted forms but
@@ -533,4 +516,4 @@ fi
533
516
 
534
517
  write_success_marker
535
518
  echo "[claude-smart] install complete. Backend and dashboard auto-start on session start." >&2
536
- echo '{"continue":true,"suppressOutput":true}'
519
+ claude_smart_emit_continue
@@ -33,10 +33,11 @@ import time
33
33
  from dataclasses import dataclass
34
34
  from pathlib import Path
35
35
 
36
- from claude_smart import context_format, ids, publish, state
36
+ from claude_smart import context_format, env_config, ids, publish, state
37
37
  from claude_smart.reflexio_adapter import Adapter
38
38
 
39
- _REFLEXIO_ENV_PATH = Path.home() / ".reflexio" / ".env"
39
+ _REFLEXIO_ENV_PATH = env_config.REFLEXIO_ENV_PATH
40
+ _MANAGED_REFLEXIO_URL = env_config.MANAGED_REFLEXIO_URL
40
41
  _DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart"
41
42
  _PLUGIN_SPEC = "claude-smart@reflexioai"
42
43
  _CODEX_MARKETPLACE_NAME = "reflexioai"
@@ -70,6 +71,16 @@ _BACKEND_SCRIPT = _SCRIPTS_DIR / "backend-service.sh"
70
71
  _DASHBOARD_SCRIPT = _SCRIPTS_DIR / "dashboard-service.sh"
71
72
  _REFLEXIO_DIR = Path.home() / ".reflexio"
72
73
  _STATE_DIR = Path.home() / ".claude-smart"
74
+ _LOCAL_DATA_NOTICE = (
75
+ "Local data was kept so reinstalling claude-smart can reuse your learned "
76
+ "rules, sessions, logs, and local Reflexio data.\n"
77
+ "Kept folders:\n"
78
+ " ~/.claude-smart\n"
79
+ " ~/.reflexio\n"
80
+ "Delete them only if you want a full reset or need to remove local "
81
+ "claude-smart data from this machine:\n"
82
+ " rm -rf ~/.claude-smart ~/.reflexio\n"
83
+ )
73
84
  _INSTALL_FAILURE_MARKER = _STATE_DIR / "install-failed"
74
85
  _DEFAULT_STORAGE_ROOT = _REFLEXIO_DIR / "data"
75
86
  _REFLEXIO_CONFIG_PATH = _REFLEXIO_DIR / "configs" / "config_self-host-org.json"
@@ -127,9 +138,21 @@ def _seed_reflexio_env() -> list[str]:
127
138
  prefix = "" if not existing or existing.endswith("\n") else "\n"
128
139
  with _REFLEXIO_ENV_PATH.open("a") as fh:
129
140
  fh.write(prefix + "\n".join(f"{f}=1" for f in missing) + "\n")
141
+ _REFLEXIO_ENV_PATH.chmod(0o600)
130
142
  return missing
131
143
 
132
144
 
145
+ def _seed_managed_reflexio_env(*, api_key: str, reflexio_url: str) -> list[str]:
146
+ """Write managed Reflexio connection settings to ``~/.reflexio/.env``."""
147
+ return env_config.set_env_vars(
148
+ _REFLEXIO_ENV_PATH,
149
+ {
150
+ env_config.REFLEXIO_URL_ENV: reflexio_url,
151
+ env_config.REFLEXIO_API_KEY_ENV: api_key,
152
+ },
153
+ )
154
+
155
+
133
156
  def _find_claude_code_plugin_root() -> Path | None:
134
157
  """Locate the installed Claude Code plugin root after native install."""
135
158
  cache_root = (
@@ -162,10 +185,9 @@ def _find_claude_code_plugin_root() -> Path | None:
162
185
  ]
163
186
  )
164
187
  for candidate in candidates:
165
- if (
166
- (candidate / "pyproject.toml").is_file()
167
- and (candidate / "scripts" / "smart-install.sh").is_file()
168
- ):
188
+ if (candidate / "pyproject.toml").is_file() and (
189
+ candidate / "scripts" / "smart-install.sh"
190
+ ).is_file():
169
191
  return candidate
170
192
  return None
171
193
 
@@ -205,7 +227,8 @@ def _bootstrap_claude_code_install() -> tuple[bool, str]:
205
227
  if result.returncode != 0:
206
228
  return False, f"smart-install.sh failed in {plugin_root}"
207
229
  if _INSTALL_FAILURE_MARKER.is_file():
208
- reason = _INSTALL_FAILURE_MARKER.read_text().strip() or "unknown error"
230
+ first_line = _INSTALL_FAILURE_MARKER.read_text().splitlines()
231
+ reason = (first_line[0].strip() if first_line else "") or "unknown error"
209
232
  return False, reason
210
233
  return True, str(plugin_root)
211
234
 
@@ -734,7 +757,26 @@ def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
734
757
  return False, output or "Codex CLI does not expose plugin marketplace commands"
735
758
 
736
759
 
737
- def cmd_install_codex(_args: argparse.Namespace) -> int:
760
+ def _configure_reflexio_setup(args: argparse.Namespace) -> None:
761
+ api_key = (getattr(args, "api_key", "") or "").strip()
762
+ if api_key:
763
+ reflexio_url = (
764
+ getattr(args, "reflexio_url", "") or _MANAGED_REFLEXIO_URL
765
+ ).strip()
766
+ added = _seed_managed_reflexio_env(api_key=api_key, reflexio_url=reflexio_url)
767
+ changed = ", ".join(added) if added else "managed Reflexio settings"
768
+ sys.stdout.write(
769
+ f"Configured {_REFLEXIO_ENV_PATH} for managed Reflexio "
770
+ f"({changed}; API key {env_config.mask_secret(api_key)}).\n"
771
+ )
772
+ return
773
+
774
+ added = _seed_reflexio_env()
775
+ if added:
776
+ sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
777
+
778
+
779
+ def cmd_install_codex(args: argparse.Namespace) -> int:
738
780
  """Install the claude-smart plugin marketplace for Codex.
739
781
 
740
782
  Only supported from a source checkout — the wheel ships the Python
@@ -766,9 +808,7 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
766
808
  return 1
767
809
  marketplace_root = _prepare_codex_local_marketplace()
768
810
 
769
- added = _seed_reflexio_env()
770
- if added:
771
- sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
811
+ _configure_reflexio_setup(args)
772
812
 
773
813
  hooks_ok, hooks_msg = _enable_codex_plugin_hooks()
774
814
  if hooks_ok:
@@ -870,9 +910,7 @@ def cmd_install(args: argparse.Namespace) -> int:
870
910
  sys.stderr.write(f"error: {' '.join(cmd)} failed (exit {exc.returncode})\n")
871
911
  return exc.returncode or 1
872
912
 
873
- added = _seed_reflexio_env()
874
- if added:
875
- sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
913
+ _configure_reflexio_setup(args)
876
914
 
877
915
  bootstrapped, message = _bootstrap_claude_code_install()
878
916
  if not bootstrapped:
@@ -952,8 +990,7 @@ def cmd_uninstall(_args: argparse.Namespace) -> int:
952
990
 
953
991
  sys.stdout.write(
954
992
  "\nclaude-smart uninstalled. Restart Claude Code to apply.\n"
955
- "Local data in ~/.reflexio/ and ~/.claude-smart/ was left in place — "
956
- "remove manually if desired.\n"
993
+ f"{_LOCAL_DATA_NOTICE}"
957
994
  )
958
995
  return 0
959
996
 
@@ -990,8 +1027,8 @@ def cmd_uninstall_codex(_args: argparse.Namespace) -> int:
990
1027
  sys.stderr.write(f"warning: could not update {_CODEX_CONFIG_PATH}\n")
991
1028
  sys.stdout.write(
992
1029
  "claude-smart Codex plugin and marketplace state removed. Restart Codex to apply. "
993
- "Codex's global hook feature flags and local data under ~/.reflexio "
994
- "and ~/.claude-smart were left in place.\n"
1030
+ "Codex's global hook feature flags were left in place.\n"
1031
+ f"{_LOCAL_DATA_NOTICE}"
995
1032
  )
996
1033
  return 0
997
1034
 
@@ -1025,6 +1062,13 @@ def cmd_show(args: argparse.Namespace) -> int:
1025
1062
  agent_playbooks=agent_playbooks,
1026
1063
  profiles=profiles,
1027
1064
  )
1065
+ if not md and adapter.read_errors:
1066
+ sys.stdout.write(
1067
+ "Could not read skills or preferences from Reflexio for "
1068
+ f"project `{project_id}` at `{adapter.url}`.\n"
1069
+ f"First error: {adapter.read_errors[0]}\n"
1070
+ )
1071
+ return 1
1028
1072
  sys.stdout.write(
1029
1073
  md or f"_No skills or preferences yet for project `{project_id}`._\n"
1030
1074
  )
@@ -1034,8 +1078,10 @@ def cmd_show(args: argparse.Namespace) -> int:
1034
1078
  def cmd_learn(args: argparse.Namespace) -> int:
1035
1079
  """Force reflexio extraction over the active session's interactions.
1036
1080
 
1037
- Publishes unpublished interactions with ``force_extraction=True`` so
1038
- extraction runs immediately rather than at the next batch interval.
1081
+ Publishes unpublished interactions with ``force_extraction=True`` and
1082
+ ``override_learning_stall=True`` as an explicit retry request rather than
1083
+ waiting for the next batch interval. The publish call itself remains
1084
+ non-blocking: ``Adapter.publish`` sends ``wait_for_response=False``.
1039
1085
  When ``args.note`` is provided, the note is appended to the session
1040
1086
  buffer as a neutral User turn before publishing — letting the user
1041
1087
  capture an explicit insight, preference, or workflow note alongside
@@ -1073,6 +1119,7 @@ def cmd_learn(args: argparse.Namespace) -> int:
1073
1119
  session_id=session_id,
1074
1120
  project_id=project_id,
1075
1121
  force_extraction=True,
1122
+ override_learning_stall=True,
1076
1123
  skip_aggregation=False,
1077
1124
  )
1078
1125
  if status == "ok":
@@ -1561,6 +1608,113 @@ def cmd_clear_all(args: argparse.Namespace) -> int:
1561
1608
  return start_rc or 0
1562
1609
 
1563
1610
 
1611
+ def _resolve_reflexio_url() -> str:
1612
+ """Return the URL endpoint for the local claude-smart reflexio backend.
1613
+
1614
+ Mirrors ``claude_smart.reflexio_adapter`` so the CLI's one-shot
1615
+ ReflexioClient calls hit the same server as the long-lived adapter
1616
+ used by hook handlers.
1617
+
1618
+ Returns:
1619
+ str: The ``REFLEXIO_URL`` env var if set, otherwise the default
1620
+ local backend endpoint.
1621
+ """
1622
+ env_config.load_reflexio_env()
1623
+ return os.environ.get("REFLEXIO_URL", "http://localhost:8071/")
1624
+
1625
+
1626
+ def _resolve_reflexio_api_key() -> str:
1627
+ env_config.load_reflexio_env()
1628
+ return os.environ.get("REFLEXIO_API_KEY", "")
1629
+
1630
+
1631
+ def _format_deleted_counts(deleted_counts: object) -> str:
1632
+ """Render ``deleted_counts`` from a ``clear_user_data`` response as a summary.
1633
+
1634
+ The backend returns one count per affected table. We surface the
1635
+ total in the headline string (``"removed N row(s)"``) and append a
1636
+ per-table breakdown when at least one bucket is populated, so the
1637
+ operator can tell *what* got cleared at a glance.
1638
+
1639
+ Args:
1640
+ deleted_counts (object): The ``deleted_counts`` field from the
1641
+ backend response. Expected to be a mapping of
1642
+ ``str -> int``; anything else is rendered as ``"unknown"``.
1643
+
1644
+ Returns:
1645
+ str: A short human-readable summary suitable for echoing to
1646
+ stdout (e.g. ``"3 row(s) (interactions=3)"``).
1647
+ """
1648
+ if not isinstance(deleted_counts, dict):
1649
+ return "unknown row(s)"
1650
+ pairs: list[tuple[str, int]] = []
1651
+ for key, value in deleted_counts.items():
1652
+ if isinstance(key, str) and isinstance(value, int):
1653
+ pairs.append((key, value))
1654
+ total = sum(value for _, value in pairs)
1655
+ if not pairs:
1656
+ return f"{total} row(s)"
1657
+ breakdown = ", ".join(f"{name}={count}" for name, count in sorted(pairs))
1658
+ return f"{total} row(s) ({breakdown})"
1659
+
1660
+
1661
+ def cmd_clear_user(args: argparse.Namespace) -> int:
1662
+ """Delete one user_id's playbooks, profiles, and interactions via the backend.
1663
+
1664
+ Per-user-scoped counterpart of ``clear-all``. Where ``clear-all`` does
1665
+ a filesystem-level wipe of the entire local storage root, ``clear-user``
1666
+ routes through the running reflexio backend's ``clear_user_data``
1667
+ endpoint so it can scope the deletion to a single ``user_id`` —
1668
+ leaving other users' data and globally-shared ``agent_playbooks``
1669
+ untouched. This is the primitive SWE-bench-style benchmark harnesses
1670
+ need when running many parallel evaluations under distinct synthetic
1671
+ user ids against one shared backend.
1672
+
1673
+ Args:
1674
+ args (argparse.Namespace): Parsed CLI args. Requires
1675
+ ``args.user_id`` (positional) and ``args.yes`` (the
1676
+ destructive-action confirmation flag, matching ``clear-all``).
1677
+
1678
+ Returns:
1679
+ int: 0 on success, 1 if confirmation is missing or the backend
1680
+ call fails.
1681
+ """
1682
+ user_id = args.user_id
1683
+ if not args.yes:
1684
+ sys.stdout.write(
1685
+ f"This will permanently delete all reflexio rows for user '{user_id}' "
1686
+ "(playbooks, profiles, interactions). Other users' data and "
1687
+ "agent_playbooks are not affected.\n"
1688
+ "Re-run with --yes to confirm.\n"
1689
+ )
1690
+ return 1
1691
+
1692
+ try:
1693
+ from reflexio import ReflexioClient # type: ignore[import-not-found]
1694
+ except ImportError as exc:
1695
+ sys.stderr.write(f"error: reflexio is not installed: {exc}\n")
1696
+ return 1
1697
+
1698
+ try:
1699
+ client = ReflexioClient(
1700
+ url_endpoint=_resolve_reflexio_url(),
1701
+ api_key=_resolve_reflexio_api_key(),
1702
+ )
1703
+ # The companion reflexio PR adds ``clear_user_data``; keep the
1704
+ # CLI buildable until both sides ship together.
1705
+ response = client.clear_user_data(user_id) # pyright: ignore[reportAttributeAccessIssue]
1706
+ except Exception as exc: # noqa: BLE001 — surface backend failure verbatim.
1707
+ sys.stderr.write(f"error: clear-user failed: {exc}\n")
1708
+ return 1
1709
+
1710
+ deleted_counts = (
1711
+ response.get("deleted_counts") if isinstance(response, dict) else None
1712
+ )
1713
+ summary = _format_deleted_counts(deleted_counts)
1714
+ sys.stdout.write(f"Cleared user '{user_id}': removed {summary}.\n")
1715
+ return 0
1716
+
1717
+
1564
1718
  def _build_parser() -> argparse.ArgumentParser:
1565
1719
  parser = argparse.ArgumentParser(prog="claude-smart")
1566
1720
  sub = parser.add_subparsers(dest="command", required=True)
@@ -1577,6 +1731,16 @@ def _build_parser() -> argparse.ArgumentParser:
1577
1731
  default=_DEFAULT_MARKETPLACE_SOURCE,
1578
1732
  help="Marketplace ref — GitHub owner/repo, or a local directory path",
1579
1733
  )
1734
+ inst.add_argument(
1735
+ "--api-key",
1736
+ default="",
1737
+ help="Configure managed Reflexio with this API key instead of local mode",
1738
+ )
1739
+ inst.add_argument(
1740
+ "--reflexio-url",
1741
+ default=_MANAGED_REFLEXIO_URL,
1742
+ help="Managed Reflexio URL used only when --api-key is provided",
1743
+ )
1580
1744
  inst.set_defaults(func=cmd_install)
1581
1745
 
1582
1746
  upd = sub.add_parser("update", help="Update claude-smart to the latest version")
@@ -1625,6 +1789,25 @@ def _build_parser() -> argparse.ArgumentParser:
1625
1789
  )
1626
1790
  ca.set_defaults(func=cmd_clear_all)
1627
1791
 
1792
+ cu = sub.add_parser(
1793
+ "clear-user",
1794
+ help=(
1795
+ "Delete one user_id's playbooks, profiles, and interactions via "
1796
+ "the backend (leaves other users and agent_playbooks intact)"
1797
+ ),
1798
+ )
1799
+ cu.add_argument(
1800
+ "user_id",
1801
+ help="User id whose reflexio rows should be cleared",
1802
+ )
1803
+ cu.add_argument(
1804
+ "--yes",
1805
+ "-y",
1806
+ action="store_true",
1807
+ help="Confirm the destructive clear without prompting",
1808
+ )
1809
+ cu.set_defaults(func=cmd_clear_user)
1810
+
1628
1811
  rs = sub.add_parser(
1629
1812
  "restart",
1630
1813
  help="Restart the reflexio backend and dashboard to pick up changes",
@@ -1649,6 +1832,7 @@ def _build_parser() -> argparse.ArgumentParser:
1649
1832
 
1650
1833
 
1651
1834
  def main(argv: list[str] | None = None) -> int:
1835
+ env_config.load_reflexio_env()
1652
1836
  parser = _build_parser()
1653
1837
  args = parser.parse_args(argv)
1654
1838
  return args.func(args)