claude-smart 0.2.22 → 0.2.24
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/.agents/plugins/marketplace.json +20 -0
- package/README.md +69 -27
- package/bin/claude-smart.js +296 -11
- package/package.json +11 -1
- package/plugin/.claude-plugin/plugin.json +17 -0
- package/plugin/.codex-plugin/plugin.json +35 -0
- package/plugin/LICENSE +202 -0
- package/plugin/README.md +37 -0
- package/plugin/bin/cs-cite +77 -0
- package/plugin/commands/clear-all.md +8 -0
- package/plugin/commands/dashboard.md +8 -0
- package/plugin/commands/learn.md +12 -0
- package/plugin/commands/restart.md +8 -0
- package/plugin/commands/show.md +8 -0
- package/plugin/dashboard/AGENTS.md +6 -0
- package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
- package/plugin/dashboard/app/api/config/route.ts +16 -0
- package/plugin/dashboard/app/api/health/route.ts +10 -0
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
- package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
- package/plugin/dashboard/app/api/sessions/route.ts +14 -0
- package/plugin/dashboard/app/configure/env/page.tsx +318 -0
- package/plugin/dashboard/app/configure/layout.tsx +47 -0
- package/plugin/dashboard/app/configure/page.tsx +5 -0
- package/plugin/dashboard/app/configure/server/page.tsx +258 -0
- package/plugin/dashboard/app/dashboard/page.tsx +227 -0
- package/plugin/dashboard/app/globals.css +129 -0
- package/plugin/dashboard/app/icon.png +0 -0
- package/plugin/dashboard/app/layout.tsx +40 -0
- package/plugin/dashboard/app/page.tsx +5 -0
- package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
- package/plugin/dashboard/app/preferences/page.tsx +126 -0
- package/plugin/dashboard/app/providers.tsx +12 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
- package/plugin/dashboard/app/sessions/page.tsx +186 -0
- package/plugin/dashboard/app/skills/page.tsx +362 -0
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
- package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
- package/plugin/dashboard/components/common/empty-state.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
- package/plugin/dashboard/components/common/page-header.tsx +34 -0
- package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
- package/plugin/dashboard/components/common/stat-card.tsx +38 -0
- package/plugin/dashboard/components/layout/nav-items.ts +22 -0
- package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
- package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
- package/plugin/dashboard/components/stall-banner.tsx +53 -0
- package/plugin/dashboard/components/ui/badge.tsx +52 -0
- package/plugin/dashboard/components/ui/button.tsx +60 -0
- package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
- package/plugin/dashboard/components/ui/input.tsx +20 -0
- package/plugin/dashboard/components/ui/label.tsx +20 -0
- package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
- package/plugin/dashboard/components/ui/select.tsx +201 -0
- package/plugin/dashboard/components/ui/separator.tsx +25 -0
- package/plugin/dashboard/components/ui/sheet.tsx +135 -0
- package/plugin/dashboard/components/ui/switch.tsx +32 -0
- package/plugin/dashboard/components.json +25 -0
- package/plugin/dashboard/eslint.config.mjs +16 -0
- package/plugin/dashboard/hooks/use-settings.tsx +88 -0
- package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
- package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
- package/plugin/dashboard/lib/config-file.ts +131 -0
- package/plugin/dashboard/lib/format.ts +58 -0
- package/plugin/dashboard/lib/reflexio-client.ts +238 -0
- package/plugin/dashboard/lib/reflexio-url.ts +17 -0
- package/plugin/dashboard/lib/session-reader.ts +245 -0
- package/plugin/dashboard/lib/status.ts +24 -0
- package/plugin/dashboard/lib/types.ts +145 -0
- package/plugin/dashboard/lib/utils.ts +6 -0
- package/plugin/dashboard/next.config.ts +7 -0
- package/plugin/dashboard/package-lock.json +10275 -0
- package/plugin/dashboard/package.json +37 -0
- package/plugin/dashboard/postcss.config.mjs +7 -0
- package/plugin/dashboard/public/claude-smart-icon.png +0 -0
- package/plugin/dashboard/tsconfig.json +34 -0
- package/plugin/hooks/codex-hooks.json +67 -0
- package/plugin/hooks/hooks.json +111 -0
- package/plugin/pyproject.toml +49 -0
- package/plugin/scripts/_codex_env.sh +27 -0
- package/plugin/scripts/_lib.sh +325 -0
- package/plugin/scripts/backend-service.sh +208 -0
- package/plugin/scripts/cli.sh +40 -0
- package/plugin/scripts/dashboard-build.sh +139 -0
- package/plugin/scripts/dashboard-open.sh +107 -0
- package/plugin/scripts/dashboard-service.sh +195 -0
- package/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/scripts/hook_entry.sh +70 -0
- package/plugin/scripts/smart-install.sh +411 -0
- package/plugin/src/claude_smart/__init__.py +3 -0
- package/plugin/src/claude_smart/cli.py +1273 -0
- package/plugin/src/claude_smart/context_format.py +277 -0
- package/plugin/src/claude_smart/context_inject.py +92 -0
- package/plugin/src/claude_smart/cs_cite.py +236 -0
- package/plugin/src/claude_smart/events/__init__.py +1 -0
- package/plugin/src/claude_smart/events/post_tool.py +148 -0
- package/plugin/src/claude_smart/events/pre_tool.py +52 -0
- package/plugin/src/claude_smart/events/session_end.py +20 -0
- package/plugin/src/claude_smart/events/session_start.py +119 -0
- package/plugin/src/claude_smart/events/stop.py +393 -0
- package/plugin/src/claude_smart/events/user_prompt.py +73 -0
- package/plugin/src/claude_smart/hook.py +114 -0
- package/plugin/src/claude_smart/ids.py +56 -0
- package/plugin/src/claude_smart/internal_call.py +89 -0
- package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
- package/plugin/src/claude_smart/publish.py +71 -0
- package/plugin/src/claude_smart/query_compose.py +51 -0
- package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
- package/plugin/src/claude_smart/runtime.py +52 -0
- package/plugin/src/claude_smart/stall_banner.py +61 -0
- package/plugin/src/claude_smart/state.py +276 -0
- 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}'
|