claude-smart 0.2.23 → 0.2.25

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 (113) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +76 -28
  3. package/bin/claude-smart.js +355 -11
  4. package/package.json +11 -1
  5. package/plugin/.claude-plugin/plugin.json +17 -0
  6. package/plugin/.codex-plugin/plugin.json +35 -0
  7. package/plugin/LICENSE +202 -0
  8. package/plugin/README.md +37 -0
  9. package/plugin/bin/cs-cite +77 -0
  10. package/plugin/commands/clear-all.md +8 -0
  11. package/plugin/commands/dashboard.md +8 -0
  12. package/plugin/commands/learn.md +12 -0
  13. package/plugin/commands/restart.md +8 -0
  14. package/plugin/commands/show.md +8 -0
  15. package/plugin/dashboard/AGENTS.md +6 -0
  16. package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
  17. package/plugin/dashboard/app/api/config/route.ts +16 -0
  18. package/plugin/dashboard/app/api/health/route.ts +10 -0
  19. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
  20. package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
  21. package/plugin/dashboard/app/api/sessions/route.ts +14 -0
  22. package/plugin/dashboard/app/configure/env/page.tsx +318 -0
  23. package/plugin/dashboard/app/configure/layout.tsx +47 -0
  24. package/plugin/dashboard/app/configure/page.tsx +5 -0
  25. package/plugin/dashboard/app/configure/server/page.tsx +258 -0
  26. package/plugin/dashboard/app/dashboard/page.tsx +227 -0
  27. package/plugin/dashboard/app/globals.css +129 -0
  28. package/plugin/dashboard/app/icon.png +0 -0
  29. package/plugin/dashboard/app/layout.tsx +40 -0
  30. package/plugin/dashboard/app/page.tsx +5 -0
  31. package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
  32. package/plugin/dashboard/app/preferences/page.tsx +126 -0
  33. package/plugin/dashboard/app/providers.tsx +12 -0
  34. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
  35. package/plugin/dashboard/app/sessions/page.tsx +186 -0
  36. package/plugin/dashboard/app/skills/page.tsx +362 -0
  37. package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
  38. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
  39. package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
  40. package/plugin/dashboard/components/common/empty-state.tsx +34 -0
  41. package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
  42. package/plugin/dashboard/components/common/page-header.tsx +34 -0
  43. package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
  44. package/plugin/dashboard/components/common/stat-card.tsx +38 -0
  45. package/plugin/dashboard/components/layout/nav-items.ts +22 -0
  46. package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
  47. package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
  48. package/plugin/dashboard/components/stall-banner.tsx +53 -0
  49. package/plugin/dashboard/components/ui/badge.tsx +52 -0
  50. package/plugin/dashboard/components/ui/button.tsx +60 -0
  51. package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
  52. package/plugin/dashboard/components/ui/input.tsx +20 -0
  53. package/plugin/dashboard/components/ui/label.tsx +20 -0
  54. package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
  55. package/plugin/dashboard/components/ui/select.tsx +201 -0
  56. package/plugin/dashboard/components/ui/separator.tsx +25 -0
  57. package/plugin/dashboard/components/ui/sheet.tsx +135 -0
  58. package/plugin/dashboard/components/ui/switch.tsx +32 -0
  59. package/plugin/dashboard/components.json +25 -0
  60. package/plugin/dashboard/eslint.config.mjs +16 -0
  61. package/plugin/dashboard/hooks/use-settings.tsx +88 -0
  62. package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
  63. package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
  64. package/plugin/dashboard/lib/config-file.ts +131 -0
  65. package/plugin/dashboard/lib/format.ts +58 -0
  66. package/plugin/dashboard/lib/reflexio-client.ts +238 -0
  67. package/plugin/dashboard/lib/reflexio-url.ts +17 -0
  68. package/plugin/dashboard/lib/session-reader.ts +245 -0
  69. package/plugin/dashboard/lib/status.ts +24 -0
  70. package/plugin/dashboard/lib/types.ts +145 -0
  71. package/plugin/dashboard/lib/utils.ts +6 -0
  72. package/plugin/dashboard/next.config.ts +7 -0
  73. package/plugin/dashboard/package-lock.json +10275 -0
  74. package/plugin/dashboard/package.json +37 -0
  75. package/plugin/dashboard/postcss.config.mjs +7 -0
  76. package/plugin/dashboard/public/claude-smart-icon.png +0 -0
  77. package/plugin/dashboard/tsconfig.json +34 -0
  78. package/plugin/hooks/codex-hooks.json +67 -0
  79. package/plugin/hooks/hooks.json +111 -0
  80. package/plugin/pyproject.toml +49 -0
  81. package/plugin/scripts/_codex_env.sh +27 -0
  82. package/plugin/scripts/_lib.sh +325 -0
  83. package/plugin/scripts/backend-service.sh +208 -0
  84. package/plugin/scripts/cli.sh +40 -0
  85. package/plugin/scripts/dashboard-build.sh +139 -0
  86. package/plugin/scripts/dashboard-open.sh +107 -0
  87. package/plugin/scripts/dashboard-service.sh +195 -0
  88. package/plugin/scripts/ensure-plugin-root.sh +84 -0
  89. package/plugin/scripts/hook_entry.sh +70 -0
  90. package/plugin/scripts/smart-install.sh +411 -0
  91. package/plugin/src/claude_smart/__init__.py +3 -0
  92. package/plugin/src/claude_smart/cli.py +1342 -0
  93. package/plugin/src/claude_smart/context_format.py +277 -0
  94. package/plugin/src/claude_smart/context_inject.py +92 -0
  95. package/plugin/src/claude_smart/cs_cite.py +236 -0
  96. package/plugin/src/claude_smart/events/__init__.py +1 -0
  97. package/plugin/src/claude_smart/events/post_tool.py +148 -0
  98. package/plugin/src/claude_smart/events/pre_tool.py +52 -0
  99. package/plugin/src/claude_smart/events/session_end.py +20 -0
  100. package/plugin/src/claude_smart/events/session_start.py +119 -0
  101. package/plugin/src/claude_smart/events/stop.py +393 -0
  102. package/plugin/src/claude_smart/events/user_prompt.py +73 -0
  103. package/plugin/src/claude_smart/hook.py +114 -0
  104. package/plugin/src/claude_smart/ids.py +56 -0
  105. package/plugin/src/claude_smart/internal_call.py +89 -0
  106. package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
  107. package/plugin/src/claude_smart/publish.py +71 -0
  108. package/plugin/src/claude_smart/query_compose.py +51 -0
  109. package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
  110. package/plugin/src/claude_smart/runtime.py +52 -0
  111. package/plugin/src/claude_smart/stall_banner.py +61 -0
  112. package/plugin/src/claude_smart/state.py +276 -0
  113. package/plugin/uv.lock +3720 -0
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # Maintain ~/.reflexio/plugin-root as a symlink to the active plugin
3
+ # install dir so slash commands can reference one short path regardless
4
+ # of whether the user is on the remote marketplace (reflexioai) or the
5
+ # local-dev marketplace (reflexioai-local).
6
+ #
7
+ # Usage: ensure-plugin-root.sh <target-dir> [--force]
8
+ # --force overwrite any existing link (used by setup-local-dev.sh)
9
+ # default self-heal only if the link is missing or points at an
10
+ # invalid target
11
+ set -eu
12
+
13
+ TARGET="${1:-}"
14
+ FORCE="${2:-}"
15
+
16
+ if [ -z "$TARGET" ]; then
17
+ echo "[claude-smart] ensure-plugin-root: usage: $0 <target-dir> [--force]" >&2
18
+ exit 1
19
+ fi
20
+
21
+ if [ ! -f "$TARGET/pyproject.toml" ]; then
22
+ echo "[claude-smart] ensure-plugin-root: $TARGET is not a plugin dir (no pyproject.toml)" >&2
23
+ exit 1
24
+ fi
25
+
26
+ LINK="$HOME/.reflexio/plugin-root"
27
+ mkdir -p "$(dirname "$LINK")"
28
+
29
+ if [ "$FORCE" = "--force" ]; then
30
+ ln -sfn "$TARGET" "$LINK"
31
+ echo "[claude-smart] plugin-root → $TARGET (forced)" >&2
32
+ exit 0
33
+ fi
34
+
35
+ # Opt-in: when CLAUDE_SMART_PLUGIN_ROOT_FOLLOW_SESSION=1 (set in the
36
+ # environment or in ~/.reflexio/.env), always relink to $TARGET so the
37
+ # symlink tracks the currently loaded plugin. Off by default to preserve
38
+ # a pinned local-dev link across sessions that load the remote plugin.
39
+ FOLLOW="${CLAUDE_SMART_PLUGIN_ROOT_FOLLOW_SESSION:-}"
40
+ if [ -z "$FOLLOW" ] && [ -f "$HOME/.reflexio/.env" ]; then
41
+ FOLLOW="$(grep -E '^CLAUDE_SMART_PLUGIN_ROOT_FOLLOW_SESSION=' "$HOME/.reflexio/.env" \
42
+ | tail -n1 | cut -d= -f2-)"
43
+ # Strip a single pair of surrounding double or single quotes, if present.
44
+ FOLLOW="${FOLLOW#\"}"; FOLLOW="${FOLLOW%\"}"
45
+ FOLLOW="${FOLLOW#\'}"; FOLLOW="${FOLLOW%\'}"
46
+ fi
47
+ if [ "$FOLLOW" = "1" ]; then
48
+ ln -sfn "$TARGET" "$LINK"
49
+ echo "[claude-smart] plugin-root → $TARGET (follow-session)" >&2
50
+ exit 0
51
+ fi
52
+
53
+ # Cache-tracking: if the link currently resolves to a path under the
54
+ # managed plugin cache (~/.claude/plugins/cache/), always retarget it to
55
+ # $TARGET. Plugin updates leave old version directories behind, so a
56
+ # valid pyproject.toml at the stale target is not proof the link is
57
+ # fresh. Links pointing outside the cache (e.g., a user's local-dev
58
+ # checkout) are left alone here and handled by the self-heal below.
59
+ if [ -L "$LINK" ]; then
60
+ # Literal target string, not realpath: we compare against what was written by ln -s.
61
+ CURRENT="$(readlink "$LINK" 2>/dev/null || true)"
62
+ case "$CURRENT" in
63
+ "$HOME/.claude/plugins/cache/"*)
64
+ CURRENT_NORM="${CURRENT%/}"
65
+ TARGET_NORM="${TARGET%/}"
66
+ if [ "$CURRENT_NORM" != "$TARGET_NORM" ]; then
67
+ ln -sfn "$TARGET" "$LINK"
68
+ echo "[claude-smart] plugin-root → $TARGET (cache-tracking, was $CURRENT)" >&2
69
+ fi
70
+ exit 0
71
+ ;;
72
+ esac
73
+ fi
74
+
75
+ # Self-heal path: only rewrite the link if it's missing or its target is
76
+ # gone/invalid. This preserves a valid local-dev symlink set earlier by
77
+ # setup-local-dev.sh, so SessionStart hooks on the local install don't
78
+ # clobber the user's repo-pointing link.
79
+ if [ -f "$LINK/pyproject.toml" ]; then
80
+ exit 0
81
+ fi
82
+
83
+ ln -sfn "$TARGET" "$LINK"
84
+ echo "[claude-smart] plugin-root → $TARGET" >&2
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # Dispatch a Claude Code or Codex hook event to the claude_smart Python package.
3
+ # CLAUDE_PLUGIN_ROOT points at the plugin dir (dev: <repo>/plugin;
4
+ # installed: ~/.claude/plugins/cache/reflexioai/claude-smart/<version>),
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.
7
+ #
8
+ # If the Setup hook recorded an install failure at
9
+ # ~/.claude-smart/install-failed, short-circuit with a user-visible
10
+ # message instead of trying to run uv and failing silently.
11
+ set -eu
12
+
13
+ HOST="claude-code"
14
+ EVENT="${1:-}"
15
+ case "$EVENT" in
16
+ claude-code|codex)
17
+ HOST="$EVENT"
18
+ EVENT="${2:-}"
19
+ ;;
20
+ esac
21
+ if [ -z "$EVENT" ]; then
22
+ echo '{"continue":true,"suppressOutput":true}'
23
+ exit 0
24
+ fi
25
+
26
+ HERE="$(cd "$(dirname "$0")" && pwd)"
27
+ # shellcheck source=_lib.sh
28
+ . "$HERE/_lib.sh"
29
+ # Pick up uv from the user's login-shell PATH (covers ~/.local/bin populated
30
+ # by the astral.sh installer) so a fresh install works before the user
31
+ # restarts their terminal. Matches the pattern used by smart-install.sh.
32
+ claude_smart_source_login_path
33
+ # Explicit fallback for the astral.sh installer's default paths, in case
34
+ # the user's login-shell rc hasn't yet been re-sourced to pick them up.
35
+ claude_smart_prepend_astral_bins
36
+
37
+ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
38
+
39
+ FAILURE_MARKER="$HOME/.claude-smart/install-failed"
40
+ if [ -f "$FAILURE_MARKER" ]; then
41
+ if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then
42
+ python3 - "$FAILURE_MARKER" <<'PY'
43
+ import json, pathlib, sys
44
+ msg = pathlib.Path(sys.argv[1]).read_text().strip() or "unknown error"
45
+ print(json.dumps({
46
+ "hookSpecificOutput": {
47
+ "hookEventName": "SessionStart",
48
+ "additionalContext": (
49
+ f"> **claude-smart is not installed correctly:** {msg}\n"
50
+ "> Re-run the plugin's Setup (restart your coding assistant) "
51
+ "or fix the underlying issue and delete "
52
+ "`~/.claude-smart/install-failed` to retry."
53
+ ),
54
+ }
55
+ }))
56
+ PY
57
+ else
58
+ echo '{"continue":true,"suppressOutput":true}'
59
+ fi
60
+ exit 0
61
+ fi
62
+
63
+ 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
67
+ fi
68
+
69
+ # Stdin is the hook payload JSON — stream it through to the Python CLI.
70
+ exec uv run --project "$PLUGIN_ROOT" --quiet python -m claude_smart.hook "$HOST" "$EVENT"
@@ -0,0 +1,411 @@
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.
5
+ #
6
+ # On failure, writes the reason to ~/.claude-smart/install-failed so
7
+ # hook_entry.sh can short-circuit and surface a user-visible message
8
+ # instead of silently no-op'ing every session.
9
+ set -eu
10
+
11
+ HERE="$(cd "$(dirname "$0")" && pwd)"
12
+ # shellcheck source=_lib.sh
13
+ . "$HERE/_lib.sh"
14
+ claude_smart_source_login_path
15
+ claude_smart_prepend_astral_bins
16
+ claude_smart_prepend_node_bins
17
+
18
+ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
19
+ REPO_ROOT="$(cd "$HERE/../.." && pwd)"
20
+
21
+ MARKER_DIR="$HOME/.claude-smart"
22
+ FAILURE_MARKER="$MARKER_DIR/install-failed"
23
+ mkdir -p "$MARKER_DIR"
24
+ rm -f "$FAILURE_MARKER"
25
+
26
+ write_failure() {
27
+ local reason
28
+ reason="$1"
29
+ printf '%s\n' "$reason" > "$FAILURE_MARKER"
30
+ echo "[claude-smart] install failed: $reason" >&2
31
+ echo '{"continue":true,"suppressOutput":true}'
32
+ exit 0
33
+ }
34
+
35
+ install_private_node() {
36
+ local NODE_MIN_MAJOR NODE_MIN_MINOR NODE_LTS_MAJOR
37
+ local node_os archive_ext reason node_arch node_platform base_url node_root
38
+ local tmp_dir shasums_path archive_name ext_re install_dir archive_path
39
+ local expected_hash actual_hash archive_win dest_win candidate_path next_link
40
+
41
+ NODE_MIN_MAJOR=20
42
+ NODE_MIN_MINOR=9
43
+ NODE_LTS_MAJOR="${CLAUDE_SMART_NODE_LTS_MAJOR:-22}"
44
+
45
+ if claude_smart_node_satisfies "$NODE_MIN_MAJOR" "$NODE_MIN_MINOR" \
46
+ && claude_smart_npm_available; then
47
+ claude_smart_clear_dashboard_unavailable
48
+ return 0
49
+ fi
50
+
51
+ case "$(uname -s 2>/dev/null)" in
52
+ Darwin*) node_os="darwin"; archive_ext="tar.gz" ;;
53
+ Linux*) node_os="linux"; archive_ext="tar.gz" ;;
54
+ MINGW*|MSYS*|CYGWIN*)
55
+ node_os="win"
56
+ archive_ext="zip"
57
+ if ! command -v powershell >/dev/null 2>&1; then
58
+ reason="PowerShell is not on PATH, so claude-smart could not extract private Node.js on Windows."
59
+ echo "[claude-smart] WARNING: $reason" >&2
60
+ claude_smart_write_dashboard_unavailable "$reason"
61
+ return 1
62
+ fi
63
+ ;;
64
+ *)
65
+ reason="unsupported OS for private Node.js install: $(uname -s 2>/dev/null || echo unknown)"
66
+ echo "[claude-smart] WARNING: $reason" >&2
67
+ claude_smart_write_dashboard_unavailable "$reason"
68
+ return 1
69
+ ;;
70
+ esac
71
+
72
+ case "$(uname -m 2>/dev/null)" in
73
+ x86_64|amd64) node_arch="x64" ;;
74
+ arm64|aarch64) node_arch="arm64" ;;
75
+ *)
76
+ reason="unsupported CPU for private Node.js install: $(uname -m 2>/dev/null || echo unknown)"
77
+ echo "[claude-smart] WARNING: $reason" >&2
78
+ claude_smart_write_dashboard_unavailable "$reason"
79
+ return 1
80
+ ;;
81
+ esac
82
+
83
+ node_platform="$node_os-$node_arch"
84
+ base_url="${CLAUDE_SMART_NODE_BASE_URL:-https://nodejs.org/dist/latest-v${NODE_LTS_MAJOR}.x}"
85
+ echo "[claude-smart] Node.js >=20.9 with npm not found — installing private Node.js from nodejs.org..." >&2
86
+ node_root="$HOME/.claude-smart/node"
87
+ mkdir -p "$node_root"
88
+ tmp_dir=$(mktemp -d "$node_root/tmp.XXXXXX") || {
89
+ reason="could not create temporary directory under $node_root"
90
+ echo "[claude-smart] WARNING: $reason" >&2
91
+ claude_smart_write_dashboard_unavailable "$reason"
92
+ return 1
93
+ }
94
+ shasums_path="$tmp_dir/SHASUMS256.txt"
95
+
96
+ if ! claude_smart_download "$base_url/SHASUMS256.txt" "$shasums_path"; then
97
+ rm -rf "$tmp_dir"
98
+ reason="could not download Node.js checksums from $base_url/SHASUMS256.txt"
99
+ echo "[claude-smart] WARNING: $reason" >&2
100
+ claude_smart_write_dashboard_unavailable "$reason"
101
+ return 1
102
+ fi
103
+
104
+ ext_re=$(printf '%s' "$archive_ext" | sed 's/\./\\./g')
105
+ archive_name=$(
106
+ awk -v platform="$node_platform" -v ext="$ext_re" \
107
+ '$2 ~ ("^node-v[0-9][^ ]*-" platform "\\." ext "$") { print $2; exit }' \
108
+ "$shasums_path"
109
+ ) || archive_name=""
110
+ if [ -z "$archive_name" ]; then
111
+ rm -rf "$tmp_dir"
112
+ reason="could not resolve Node.js archive for $node_platform from $base_url"
113
+ echo "[claude-smart] WARNING: $reason" >&2
114
+ claude_smart_write_dashboard_unavailable "$reason"
115
+ return 1
116
+ fi
117
+
118
+ install_dir="$node_root/${archive_name%.$archive_ext}"
119
+ archive_path="$tmp_dir/$archive_name"
120
+ expected_hash=$(awk -v name="$archive_name" '$2 == name { print $1; exit }' "$shasums_path")
121
+ if [ -z "$expected_hash" ]; then
122
+ rm -rf "$tmp_dir"
123
+ reason="Node.js checksums did not include $archive_name"
124
+ echo "[claude-smart] WARNING: $reason" >&2
125
+ claude_smart_write_dashboard_unavailable "$reason"
126
+ return 1
127
+ fi
128
+
129
+ if ! claude_smart_download "$base_url/$archive_name" "$archive_path"; then
130
+ rm -rf "$tmp_dir"
131
+ reason="Node.js download failed from $base_url/$archive_name"
132
+ echo "[claude-smart] WARNING: $reason" >&2
133
+ claude_smart_write_dashboard_unavailable "$reason"
134
+ return 1
135
+ fi
136
+
137
+ actual_hash=$(claude_smart_sha256_file "$archive_path" || true)
138
+ if [ -z "$actual_hash" ] || [ "$actual_hash" != "$expected_hash" ]; then
139
+ rm -rf "$tmp_dir"
140
+ reason="Node.js checksum verification failed for $archive_name"
141
+ echo "[claude-smart] WARNING: $reason" >&2
142
+ claude_smart_write_dashboard_unavailable "$reason"
143
+ return 1
144
+ fi
145
+
146
+ rm -rf "$install_dir" "$node_root/current.next"
147
+ if [ "$archive_ext" = "zip" ]; then
148
+ archive_win="$archive_path"
149
+ dest_win="$node_root"
150
+ if command -v cygpath >/dev/null 2>&1; then
151
+ archive_win=$(cygpath -w "$archive_path")
152
+ dest_win=$(cygpath -w "$node_root")
153
+ fi
154
+ if ! ARCHIVE_PATH="$archive_win" DEST_DIR="$dest_win" powershell -NoProfile -ExecutionPolicy Bypass -Command \
155
+ '$ProgressPreference="SilentlyContinue"; Expand-Archive -LiteralPath $env:ARCHIVE_PATH -DestinationPath $env:DEST_DIR -Force' >&2; then
156
+ rm -rf "$tmp_dir" "$install_dir"
157
+ reason="Node.js archive extraction failed for $archive_name"
158
+ echo "[claude-smart] WARNING: $reason" >&2
159
+ claude_smart_write_dashboard_unavailable "$reason"
160
+ return 1
161
+ fi
162
+ elif ! tar -xzf "$archive_path" -C "$node_root"; then
163
+ rm -rf "$tmp_dir" "$install_dir"
164
+ reason="Node.js archive extraction failed for $archive_name"
165
+ echo "[claude-smart] WARNING: $reason" >&2
166
+ claude_smart_write_dashboard_unavailable "$reason"
167
+ return 1
168
+ fi
169
+
170
+ if [ ! -d "$install_dir" ]; then
171
+ rm -rf "$tmp_dir"
172
+ reason="Node.js archive extracted without expected directory $install_dir"
173
+ echo "[claude-smart] WARNING: $reason" >&2
174
+ claude_smart_write_dashboard_unavailable "$reason"
175
+ return 1
176
+ fi
177
+
178
+ if [ -x "$install_dir/bin/node" ]; then
179
+ candidate_path="$install_dir/bin:$PATH"
180
+ elif [ -x "$install_dir/node.exe" ] || [ -x "$install_dir/node" ]; then
181
+ candidate_path="$install_dir:$PATH"
182
+ else
183
+ rm -rf "$tmp_dir"
184
+ reason="Node.js archive extracted without a node executable"
185
+ echo "[claude-smart] WARNING: $reason" >&2
186
+ claude_smart_write_dashboard_unavailable "$reason"
187
+ return 1
188
+ fi
189
+
190
+ if claude_smart_node_satisfies "$NODE_MIN_MAJOR" "$NODE_MIN_MINOR" \
191
+ && claude_smart_npm_available; then
192
+ : # existing PATH unexpectedly became suitable; keep going
193
+ elif PATH="$candidate_path" claude_smart_node_satisfies "$NODE_MIN_MAJOR" "$NODE_MIN_MINOR" \
194
+ && PATH="$candidate_path" claude_smart_npm_available; then
195
+ : # candidate install is suitable
196
+ else
197
+ rm -rf "$tmp_dir"
198
+ reason="private Node.js install completed but node/npm are still not usable"
199
+ echo "[claude-smart] WARNING: $reason" >&2
200
+ claude_smart_write_dashboard_unavailable "$reason"
201
+ return 1
202
+ fi
203
+
204
+ next_link="$node_root/current.next.$$"
205
+ if ln -s "$install_dir" "$next_link" 2>/dev/null; then
206
+ if mv -Tf "$next_link" "$node_root/current" 2>/dev/null; then
207
+ :
208
+ elif mv -hf "$next_link" "$node_root/current" 2>/dev/null; then
209
+ :
210
+ else
211
+ rm -rf "$node_root/current"
212
+ mv "$next_link" "$node_root/current"
213
+ fi
214
+ else
215
+ rm -rf "$next_link" "$node_root/current"
216
+ mv "$install_dir" "$node_root/current"
217
+ fi
218
+
219
+ rm -rf "$tmp_dir"
220
+ claude_smart_prepend_node_bins
221
+ if claude_smart_node_satisfies "$NODE_MIN_MAJOR" "$NODE_MIN_MINOR" \
222
+ && claude_smart_npm_available; then
223
+ claude_smart_clear_dashboard_unavailable
224
+ echo "[claude-smart] installed private $(node -v) at $node_root/current" >&2
225
+ return 0
226
+ fi
227
+
228
+ reason="private Node.js install completed but current node/npm are still not usable"
229
+ echo "[claude-smart] WARNING: $reason" >&2
230
+ claude_smart_write_dashboard_unavailable "$reason"
231
+ return 1
232
+ }
233
+
234
+ if [ "${CLAUDE_SMART_INSTALL_PRIVATE_NODE_ONLY:-}" = "1" ]; then
235
+ install_private_node
236
+ exit $?
237
+ fi
238
+
239
+ # Dev-mode only: when running from a git checkout, pull the reflexio
240
+ # submodule so tests/benchmarks can use its sources. In install mode the
241
+ # plugin lives under ~/.claude/plugins/cache and reflexio-ai resolves
242
+ # from PyPI instead. The guard checks for both `.git` and `.gitmodules`
243
+ # at REPO_ROOT to distinguish a dev checkout from a marketplace cache
244
+ # (where REPO_ROOT has neither).
245
+ if [ -d "$REPO_ROOT/.git" ] && [ -f "$REPO_ROOT/.gitmodules" ]; then
246
+ echo "[claude-smart] initializing reflexio submodule..." >&2
247
+ if ! (cd "$REPO_ROOT" && git submodule update --init --recursive reflexio) >&2; then
248
+ echo "[claude-smart] WARNING: git submodule update failed; continuing with PyPI reflexio-ai" >&2
249
+ fi
250
+ fi
251
+
252
+ if ! command -v uv >/dev/null 2>&1; then
253
+ echo "[claude-smart] uv not found — installing from astral.sh..." >&2
254
+ # The astral.sh bash installer downloads a zip and unzips it. On
255
+ # Windows-flavoured bash (Git Bash / MSYS) the bundled `unzip` corrupts
256
+ # the Windows uv binary (bad CRC on the inflated uv.exe), leaving the
257
+ # install half-finished. Use the official PowerShell installer
258
+ # (install.ps1) on Windows, which writes uv.exe to ~/.local/bin
259
+ # natively — same destination the bash installer targets on POSIX, so
260
+ # claude_smart_prepend_astral_bins picks it up uniformly afterwards.
261
+ if claude_smart_is_windows; then
262
+ if ! command -v powershell >/dev/null 2>&1; then
263
+ write_failure "uv install needs PowerShell on Windows but powershell is not on PATH — install uv manually from https://docs.astral.sh/uv/"
264
+ fi
265
+ if ! powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://astral.sh/uv/install.ps1 | iex" >&2; then
266
+ write_failure "uv install via PowerShell failed — install manually from https://docs.astral.sh/uv/"
267
+ fi
268
+ else
269
+ UV_INSTALLER="$MARKER_DIR/uv-install.sh"
270
+ if ! claude_smart_download https://astral.sh/uv/install.sh "$UV_INSTALLER"; then
271
+ write_failure "uv installer download failed — install manually from https://docs.astral.sh/uv/"
272
+ fi
273
+ if ! sh "$UV_INSTALLER" >&2; then
274
+ write_failure "uv install failed — install manually from https://docs.astral.sh/uv/"
275
+ fi
276
+ fi
277
+ claude_smart_prepend_astral_bins
278
+ if ! command -v uv >/dev/null 2>&1; then
279
+ UV_FOUND=""
280
+ for candidate in "$HOME/.local/bin/uv" "$HOME/.local/bin/uv.exe" "$HOME/.cargo/bin/uv" "$HOME/bin/uv"; do
281
+ if [ -x "$candidate" ]; then
282
+ UV_FOUND="$candidate"
283
+ break
284
+ fi
285
+ done
286
+ if [ -n "$UV_FOUND" ]; then
287
+ write_failure "uv installed at $UV_FOUND — add its parent directory to PATH in your shell rc"
288
+ else
289
+ write_failure "uv install reported success but binary not found — install manually from https://docs.astral.sh/uv/"
290
+ fi
291
+ fi
292
+ fi
293
+
294
+ cd "$PLUGIN_ROOT"
295
+ echo "[claude-smart] running uv sync..." >&2
296
+ if ! uv sync --locked --python 3.12 --quiet >&2; then
297
+ write_failure "uv sync failed in $PLUGIN_ROOT — run 'uv sync --locked --python 3.12' there to diagnose"
298
+ fi
299
+
300
+ # Reflexio's CLI reads ~/.reflexio/.env (see reflexio/cli/env_loader.py);
301
+ # append our two opt-in flags there so `reflexio services start` picks
302
+ # them up regardless of which directory the user runs it from.
303
+ REFLEXIO_ENV="$HOME/.reflexio/.env"
304
+ mkdir -p "$(dirname "$REFLEXIO_ENV")"
305
+ touch "$REFLEXIO_ENV"
306
+ if ! grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$REFLEXIO_ENV"; then
307
+ printf '\n# Route reflexio generation through the local Claude Code CLI\nCLAUDE_SMART_USE_LOCAL_CLI=1\n' >> "$REFLEXIO_ENV"
308
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_CLI=1 to $REFLEXIO_ENV" >&2
309
+ fi
310
+ if ! grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
311
+ printf '# Use the in-process ONNX embedder (chromadb) — no API key for semantic search\nCLAUDE_SMART_USE_LOCAL_EMBEDDING=1\n' >> "$REFLEXIO_ENV"
312
+ echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
313
+ fi
314
+
315
+ # Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
316
+ # plugin backend port (8071). Matches the quoted and unquoted forms but
317
+ # requires paired quotes, so malformed or deliberately different values
318
+ # (e.g. a remote reflexio URL) are preserved.
319
+ if grep -qE '^REFLEXIO_URL=("http://localhost:8081/?"|http://localhost:8081/?)$' "$REFLEXIO_ENV"; then
320
+ sed -i.bak -E \
321
+ -e 's|^REFLEXIO_URL="http://localhost:8081(/?)"$|REFLEXIO_URL="http://localhost:8071\1"|' \
322
+ -e 's|^REFLEXIO_URL=http://localhost:8081(/?)$|REFLEXIO_URL=http://localhost:8071\1|' \
323
+ "$REFLEXIO_ENV"
324
+ echo "[claude-smart] migrated REFLEXIO_URL 8081 → 8071 in $REFLEXIO_ENV (backup at $REFLEXIO_ENV.bak)" >&2
325
+ fi
326
+
327
+ if ! command -v claude >/dev/null 2>&1; then
328
+ echo "[claude-smart] WARNING: 'claude' CLI not on PATH — reflexio extractors will have no LLM until it's installed" >&2
329
+ fi
330
+
331
+ # Allowlist cs-cite globally so Claude's citation Bash calls don't pop a
332
+ # permission prompt mid-turn. Idempotent: no-ops when the entry is already
333
+ # present. Uses Python to preserve the rest of settings.json intact.
334
+ # Resolves python via claude_smart_resolve_python so we don't fire the
335
+ # Windows App Execution Alias stub (which exits non-zero with "Python
336
+ # was not found" when no real interpreter is installed).
337
+ CLAUDE_SETTINGS="$HOME/.claude/settings.json"
338
+ mkdir -p "$(dirname "$CLAUDE_SETTINGS")"
339
+ PY_BIN=$(claude_smart_resolve_python || true)
340
+ if [ -z "$PY_BIN" ]; then
341
+ echo "[claude-smart] WARNING: no working python interpreter found; skipping cs-cite allowlist" >&2
342
+ elif "$PY_BIN" - "$CLAUDE_SETTINGS" <<'PY' >&2
343
+ import json
344
+ import sys
345
+ from pathlib import Path
346
+
347
+ path = Path(sys.argv[1])
348
+ entry = "Bash(cs-cite:*)"
349
+ data: dict = {}
350
+ if path.is_file():
351
+ try:
352
+ data = json.loads(path.read_text() or "{}")
353
+ except json.JSONDecodeError:
354
+ print(
355
+ f"[claude-smart] WARNING: {path} is not valid JSON; skipping cs-cite allowlist",
356
+ file=sys.stderr,
357
+ )
358
+ sys.exit(2)
359
+ def _warn_and_exit(reason: str) -> None:
360
+ print(
361
+ f"[claude-smart] WARNING: {path} {reason}; skipping cs-cite allowlist",
362
+ file=sys.stderr,
363
+ )
364
+ sys.exit(2)
365
+
366
+ if not isinstance(data, dict):
367
+ _warn_and_exit("top-level is not a JSON object")
368
+ permissions = data.setdefault("permissions", {})
369
+ if not isinstance(permissions, dict):
370
+ _warn_and_exit("'permissions' is not a JSON object")
371
+ allow = permissions.setdefault("allow", [])
372
+ if not isinstance(allow, list):
373
+ _warn_and_exit("'permissions.allow' is not a JSON array")
374
+ if entry in allow:
375
+ sys.exit(1) # already present — convey via exit code so shell can skip the log
376
+ allow.append(entry)
377
+ path.write_text(json.dumps(data, indent=2) + "\n")
378
+ sys.exit(0)
379
+ PY
380
+ then
381
+ echo "[claude-smart] added Bash(cs-cite:*) to $CLAUDE_SETTINGS permissions.allow" >&2
382
+ fi
383
+
384
+ # Spawn the dashboard build detached so install returns immediately and
385
+ # Claude Code's install-hook timeout never kills a half-finished
386
+ # `next build` (which would force the user into a manual /claude-smart:restart
387
+ # recovery). dashboard-service.sh will also re-spawn this on SessionStart
388
+ # if .next is still missing, and dashboard-open.sh detects the build-pid
389
+ # file to surface a "still building" message instead of a generic error.
390
+ DASHBOARD_DIR="$PLUGIN_ROOT/dashboard"
391
+ if [ -d "$DASHBOARD_DIR" ]; then
392
+ install_private_node || true
393
+ fi
394
+ if [ -d "$DASHBOARD_DIR" ] && claude_smart_npm_available; then
395
+ echo "[claude-smart] starting dashboard build in background (~1-2 min on first install)" >&2
396
+ claude_smart_spawn_detached bash "$HERE/dashboard-build.sh" >/dev/null 2>&1
397
+ elif [ -d "$DASHBOARD_DIR" ]; then
398
+ reason="npm is not on PATH after private Node.js bootstrap"
399
+ echo "[claude-smart] WARNING: $reason" >&2
400
+ [ -f "$(claude_smart_dashboard_unavailable_marker)" ] || claude_smart_write_dashboard_unavailable "$reason"
401
+ fi
402
+
403
+ # Point ~/.reflexio/plugin-root at this install so slash commands can
404
+ # reference one stable short path regardless of which marketplace
405
+ # (reflexioai or reflexioai-local) loaded us.
406
+ if ! bash "$HERE/ensure-plugin-root.sh" "$PLUGIN_ROOT"; then
407
+ echo "[claude-smart] WARNING: failed to set ~/.reflexio/plugin-root symlink — slash commands may not resolve" >&2
408
+ fi
409
+
410
+ echo "[claude-smart] install complete. Backend and dashboard auto-start on session start." >&2
411
+ echo '{"continue":true,"suppressOutput":true}'
@@ -0,0 +1,3 @@
1
+ """claude-smart — self-improving Claude Code plugin via reflexio."""
2
+
3
+ __version__ = "0.1.0"