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.
- package/README.md +2 -2
- package/bin/claude-smart.js +172 -18
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/api/config/route.ts +11 -2
- package/plugin/dashboard/app/api/health/route.ts +45 -6
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +15 -7
- package/plugin/dashboard/app/api/rules/applied/route.ts +27 -0
- package/plugin/dashboard/app/configure/env/page.tsx +36 -31
- package/plugin/dashboard/app/configure/layout.tsx +1 -1
- package/plugin/dashboard/app/configure/server/page.tsx +8 -14
- package/plugin/dashboard/app/dashboard/page.tsx +311 -115
- package/plugin/dashboard/app/globals.css +80 -66
- package/plugin/dashboard/app/layout.tsx +13 -10
- package/plugin/dashboard/app/preferences/[id]/page.tsx +21 -32
- package/plugin/dashboard/app/preferences/page.tsx +154 -54
- package/plugin/dashboard/app/preferences/project/[id]/page.tsx +1 -0
- package/plugin/dashboard/app/rules/[id]/page.tsx +51 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +4 -4
- package/plugin/dashboard/app/sessions/page.tsx +14 -10
- package/plugin/dashboard/app/skills/page.tsx +175 -56
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +22 -38
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +20 -37
- package/plugin/dashboard/components/common/delete-learning-danger-zone.tsx +166 -0
- package/plugin/dashboard/components/common/empty-state.tsx +4 -2
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +5 -3
- package/plugin/dashboard/components/common/page-tabs.tsx +4 -4
- package/plugin/dashboard/components/common/stat-card.tsx +9 -5
- package/plugin/dashboard/components/layout/sidebar.tsx +24 -9
- package/plugin/dashboard/components/layout/top-bar.tsx +37 -25
- package/plugin/dashboard/components/ui/input.tsx +1 -0
- package/plugin/dashboard/hooks/use-settings.tsx +30 -61
- package/plugin/dashboard/hooks/use-stall-state.ts +5 -9
- package/plugin/dashboard/lib/config-file.ts +2 -0
- package/plugin/dashboard/lib/reflexio-client.ts +23 -48
- package/plugin/dashboard/lib/session-reader.ts +222 -6
- package/plugin/dashboard/lib/types.ts +20 -1
- package/plugin/dashboard/package-lock.json +70 -95
- package/plugin/dashboard/package.json +5 -2
- package/plugin/hooks/hooks.json +1 -1
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +126 -0
- package/plugin/scripts/backend-service.sh +32 -7
- package/plugin/scripts/cli.sh +4 -2
- package/plugin/scripts/codex-hook.js +100 -3
- package/plugin/scripts/dashboard-service.sh +98 -19
- package/plugin/scripts/hook_entry.sh +32 -11
- package/plugin/scripts/smart-install.sh +27 -44
- package/plugin/src/claude_smart/cli.py +204 -20
- package/plugin/src/claude_smart/context_format.py +244 -6
- package/plugin/src/claude_smart/context_inject.py +8 -1
- package/plugin/src/claude_smart/cs_cite.py +186 -34
- package/plugin/src/claude_smart/env_config.py +102 -0
- package/plugin/src/claude_smart/events/session_end.py +171 -6
- package/plugin/src/claude_smart/events/session_start.py +26 -2
- package/plugin/src/claude_smart/events/stop.py +48 -9
- package/plugin/src/claude_smart/hook.py +62 -4
- package/plugin/src/claude_smart/hook_log.py +301 -0
- package/plugin/src/claude_smart/internal_call.py +30 -0
- package/plugin/src/claude_smart/publish.py +5 -0
- package/plugin/src/claude_smart/reflexio_adapter.py +102 -26
- package/plugin/uv.lock +1 -1
package/plugin/scripts/_lib.sh
CHANGED
|
@@ -32,6 +32,77 @@ claude_smart_prepend_node_bins() {
|
|
|
32
32
|
export PATH="$_CS_NODE_ROOT/bin:$_CS_NODE_ROOT:$PATH"
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
claude_smart_env_unquote() {
|
|
36
|
+
local value first last
|
|
37
|
+
value="$1"
|
|
38
|
+
first="${value%"${value#?}"}"
|
|
39
|
+
last="${value#"${value%?}"}"
|
|
40
|
+
if { [ "$first" = '"' ] && [ "$last" = '"' ]; } || { [ "$first" = "'" ] && [ "$last" = "'" ]; }; then
|
|
41
|
+
value="${value#?}"
|
|
42
|
+
value="${value%?}"
|
|
43
|
+
fi
|
|
44
|
+
printf '%s\n' "$value"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
claude_smart_source_reflexio_env() {
|
|
48
|
+
local env_file line key value
|
|
49
|
+
env_file="$HOME/.reflexio/.env"
|
|
50
|
+
[ -f "$env_file" ] || return 0
|
|
51
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
52
|
+
line="${line#"${line%%[![:space:]]*}"}"
|
|
53
|
+
line="${line%"${line##*[![:space:]]}"}"
|
|
54
|
+
[ -n "$line" ] || continue
|
|
55
|
+
case "$line" in
|
|
56
|
+
\#*) continue ;;
|
|
57
|
+
export\ *) line="${line#export }" ;;
|
|
58
|
+
esac
|
|
59
|
+
key="${line%%=*}"
|
|
60
|
+
[ "$key" != "$line" ] || continue
|
|
61
|
+
value="${line#*=}"
|
|
62
|
+
key="${key%"${key##*[![:space:]]}"}"
|
|
63
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
64
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
65
|
+
case "$key" in
|
|
66
|
+
REFLEXIO_URL|REFLEXIO_API_KEY|CLAUDE_SMART_USE_LOCAL_CLI|CLAUDE_SMART_USE_LOCAL_EMBEDDING|CLAUDE_SMART_BACKEND_AUTOSTART|CLAUDE_SMART_DASHBOARD_AUTOSTART|CLAUDE_SMART_CLI_PATH|CLAUDE_SMART_CLI_TIMEOUT|CLAUDE_SMART_STATE_DIR|CLAUDE_SMART_ENABLE_OPTIMIZER)
|
|
67
|
+
if [ -z "$(eval "printf '%s' \"\${$key:-}\"")" ]; then
|
|
68
|
+
value="$(claude_smart_env_unquote "$value")"
|
|
69
|
+
export "$key=$value"
|
|
70
|
+
fi
|
|
71
|
+
;;
|
|
72
|
+
esac
|
|
73
|
+
done < "$env_file"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
claude_smart_reflexio_url_is_remote() {
|
|
77
|
+
local url
|
|
78
|
+
url="${REFLEXIO_URL:-}"
|
|
79
|
+
[ -n "$url" ] || return 1
|
|
80
|
+
case "$url" in
|
|
81
|
+
http://localhost|http://localhost/|http://localhost:*|http://127.0.0.1|http://127.0.0.1/|http://127.0.0.1:*|http://0.0.0.0|http://0.0.0.0/|http://0.0.0.0:*|http://\[::1\]|http://\[::1\]/|http://\[::1\]:*)
|
|
82
|
+
return 1
|
|
83
|
+
;;
|
|
84
|
+
esac
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
claude_smart_is_internal_invocation_env() {
|
|
89
|
+
if [ "${CLAUDE_SMART_INTERNAL:-}" = "1" ]; then
|
|
90
|
+
return 0
|
|
91
|
+
fi
|
|
92
|
+
if [ -n "${CLAUDE_CODE_ENTRYPOINT:-}" ] && [ "${CLAUDE_CODE_ENTRYPOINT:-}" != "cli" ]; then
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
return 1
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
claude_smart_emit_continue() {
|
|
99
|
+
if [ "${CLAUDE_SMART_HOST:-claude-code}" = "codex" ]; then
|
|
100
|
+
echo '{"continue":true}'
|
|
101
|
+
else
|
|
102
|
+
echo '{"continue":true,"suppressOutput":true}'
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
35
106
|
claude_smart_dashboard_unavailable_marker() {
|
|
36
107
|
printf '%s\n' "$HOME/.claude-smart/dashboard-unavailable"
|
|
37
108
|
}
|
|
@@ -196,6 +267,61 @@ PY
|
|
|
196
267
|
return 1
|
|
197
268
|
}
|
|
198
269
|
|
|
270
|
+
# Print a short fingerprint line for a file ("<cksum>:<size>" or "missing").
|
|
271
|
+
claude_smart_fingerprint_file() {
|
|
272
|
+
local path
|
|
273
|
+
path="$1"
|
|
274
|
+
if [ -f "$path" ]; then
|
|
275
|
+
cksum "$path" 2>/dev/null | awk '{print $1 ":" $2}'
|
|
276
|
+
else
|
|
277
|
+
printf 'missing\n'
|
|
278
|
+
fi
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Print a multi-line fingerprint covering every input that determines
|
|
282
|
+
# whether smart-install.sh can succeed. Order is stable so the hash is
|
|
283
|
+
# reproducible. Args: $1 = plugin_root, $2 = scripts_dir.
|
|
284
|
+
claude_smart_install_fingerprint() {
|
|
285
|
+
local plugin_root script_dir
|
|
286
|
+
plugin_root="$1"
|
|
287
|
+
script_dir="$2"
|
|
288
|
+
printf 'plugin_root=%s\n' "$plugin_root"
|
|
289
|
+
printf 'smart_install=%s\n' "$(claude_smart_fingerprint_file "$script_dir/smart-install.sh")"
|
|
290
|
+
printf 'lib=%s\n' "$(claude_smart_fingerprint_file "$script_dir/_lib.sh")"
|
|
291
|
+
printf 'pyproject=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/pyproject.toml")"
|
|
292
|
+
printf 'uv_lock=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/uv.lock")"
|
|
293
|
+
# Resolved python interpreter — catches a system upgrade (3.12.4 → 3.12.5)
|
|
294
|
+
# that would otherwise let install_complete return true against a venv
|
|
295
|
+
# built against a now-deleted interpreter.
|
|
296
|
+
if command -v uv >/dev/null 2>&1; then
|
|
297
|
+
printf 'python=%s\n' "$(uv python find 3.12 2>/dev/null || echo missing)"
|
|
298
|
+
else
|
|
299
|
+
printf 'python=no-uv\n'
|
|
300
|
+
fi
|
|
301
|
+
if [ -d "$plugin_root/dashboard" ]; then
|
|
302
|
+
printf 'dashboard_pkg=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/dashboard/package.json")"
|
|
303
|
+
printf 'dashboard_lock=%s\n' "$(claude_smart_fingerprint_file "$plugin_root/dashboard/package-lock.json")"
|
|
304
|
+
else
|
|
305
|
+
printf 'dashboard_pkg=none\n'
|
|
306
|
+
printf 'dashboard_lock=none\n'
|
|
307
|
+
fi
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Hash the install fingerprint to a single short hex string. Used by the
|
|
311
|
+
# install-failed marker so hook_entry.sh can detect when inputs change and
|
|
312
|
+
# auto-clear a stale marker. Args: $1 = plugin_root, $2 = scripts_dir.
|
|
313
|
+
claude_smart_install_fingerprint_hash() {
|
|
314
|
+
local plugin_root script_dir tmp rc
|
|
315
|
+
plugin_root="$1"
|
|
316
|
+
script_dir="$2"
|
|
317
|
+
tmp=$(mktemp 2>/dev/null) || return 1
|
|
318
|
+
claude_smart_install_fingerprint "$plugin_root" "$script_dir" > "$tmp"
|
|
319
|
+
claude_smart_sha256_file "$tmp"
|
|
320
|
+
rc=$?
|
|
321
|
+
rm -f "$tmp"
|
|
322
|
+
return $rc
|
|
323
|
+
}
|
|
324
|
+
|
|
199
325
|
# Return 0 if `node` exists and satisfies the minimum major/minor pair.
|
|
200
326
|
# Patch versions are intentionally ignored because our requirement is a
|
|
201
327
|
# floor, not an exact runtime pin.
|
|
@@ -22,18 +22,25 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
|
22
22
|
. "$HERE/_lib.sh"
|
|
23
23
|
claude_smart_source_login_path
|
|
24
24
|
claude_smart_prepend_astral_bins
|
|
25
|
+
claude_smart_source_reflexio_env
|
|
25
26
|
|
|
26
27
|
CMD="${1:-start}"
|
|
27
28
|
PORT=8071
|
|
29
|
+
EMBEDDING_PORT="${EMBEDDING_PORT:-8072}"
|
|
28
30
|
# Pass through to `reflexio services start/stop` so the spawned backend
|
|
29
31
|
# binds to PORT instead of reflexio's library default (8081).
|
|
30
32
|
export BACKEND_PORT="$PORT"
|
|
33
|
+
export EMBEDDING_PORT
|
|
31
34
|
|
|
32
35
|
# Default: route extraction through the active host CLI + ONNX embedder
|
|
33
36
|
# so claude-smart works without any LLM API key. Users can opt out by
|
|
34
37
|
# pre-exporting these to 0.
|
|
35
38
|
export CLAUDE_SMART_USE_LOCAL_CLI="${CLAUDE_SMART_USE_LOCAL_CLI:-1}"
|
|
36
39
|
export CLAUDE_SMART_USE_LOCAL_EMBEDDING="${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}"
|
|
40
|
+
if [ "${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-}" = "1" ]; then
|
|
41
|
+
export REFLEXIO_EMBEDDING_PROVIDER="${REFLEXIO_EMBEDDING_PROVIDER:-local_service}"
|
|
42
|
+
export REFLEXIO_EMBEDDING_SERVICE_URL="${REFLEXIO_EMBEDDING_SERVICE_URL:-http://127.0.0.1:$EMBEDDING_PORT}"
|
|
43
|
+
fi
|
|
37
44
|
# The backend can be spawned from contexts whose PATH lacks the host
|
|
38
45
|
# CLI dir (commonly ~/.local/bin or /opt/homebrew/bin). Pin the CLI
|
|
39
46
|
# explicitly if we can resolve it from our own (post-login-path) PATH.
|
|
@@ -61,7 +68,7 @@ LOG_MAX_BYTES="$(claude_smart_log_max_bytes)"
|
|
|
61
68
|
mkdir -p "$STATE_DIR"
|
|
62
69
|
claude_smart_trim_log_file "$LOG_FILE" "$LOG_MAX_BYTES"
|
|
63
70
|
|
|
64
|
-
emit_ok() {
|
|
71
|
+
emit_ok() { claude_smart_emit_continue; }
|
|
65
72
|
|
|
66
73
|
emit_start_failure() {
|
|
67
74
|
reason="$1"
|
|
@@ -173,6 +180,13 @@ full_stop() {
|
|
|
173
180
|
|
|
174
181
|
case "$CMD" in
|
|
175
182
|
start)
|
|
183
|
+
if claude_smart_is_internal_invocation_env; then
|
|
184
|
+
emit_ok; exit 0
|
|
185
|
+
fi
|
|
186
|
+
if claude_smart_reflexio_url_is_remote; then
|
|
187
|
+
claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: remote REFLEXIO_URL configured; skipping local backend start"
|
|
188
|
+
emit_ok; exit 0
|
|
189
|
+
fi
|
|
176
190
|
# Opt-out: users who don't want the backend managed by the hook can
|
|
177
191
|
# set CLAUDE_SMART_BACKEND_AUTOSTART=0.
|
|
178
192
|
if [ "${CLAUDE_SMART_BACKEND_AUTOSTART:-1}" = "0" ]; then
|
|
@@ -206,16 +220,21 @@ case "$CMD" in
|
|
|
206
220
|
export INTERACTION_CLEANUP_THRESHOLD="${INTERACTION_CLEANUP_THRESHOLD:-500}"
|
|
207
221
|
export INTERACTION_CLEANUP_DELETE_COUNT="${INTERACTION_CLEANUP_DELETE_COUNT:-200}"
|
|
208
222
|
|
|
209
|
-
# --no-reload: uvicorn's reloader forks a supervisor; makes
|
|
210
|
-
# bookkeeping harder and we don't need hot-reload for a user-facing
|
|
211
|
-
# service. Detach via claude_smart_spawn_detached so the same code
|
|
212
|
-
# path covers Linux (setsid), macOS (python3 os.setsid), and Windows
|
|
213
223
|
# (nohup; no process groups). backend-log-runner.sh owns stdout/stderr
|
|
214
224
|
# capture so process output cannot grow backend.log past its cap.
|
|
225
|
+
#
|
|
226
|
+
# --workers: reflexio defaults to 2 (zero-downtime worker recycling
|
|
227
|
+
# for server deployments). For a single-user Claude Code plugin
|
|
228
|
+
# that's pure overhead: ~1.1 GB extra RSS, periodic 5–10 s spawn
|
|
229
|
+
# hiccups during worker rotation, and SQLite can't accept concurrent
|
|
230
|
+
# writers anyway. Default to 1 here; opt in to N via
|
|
231
|
+
# CLAUDE_SMART_BACKEND_WORKERS for power users running concurrent
|
|
232
|
+
# Claude Code sessions or wanting zero-downtime recycling.
|
|
233
|
+
workers="${CLAUDE_SMART_BACKEND_WORKERS:-1}"
|
|
215
234
|
claude_smart_spawn_detached bash "$HERE/backend-log-runner.sh" \
|
|
216
235
|
"$LOG_FILE" "$LOG_MAX_BYTES" -- \
|
|
217
236
|
uv run --project "$PLUGIN_ROOT" --quiet \
|
|
218
|
-
reflexio services start --only backend --no-reload
|
|
237
|
+
reflexio services start --only backend --no-reload --workers "$workers"
|
|
219
238
|
svc_pid=$!
|
|
220
239
|
# Record the spawned pid, not a pgid sampled with ps. On POSIX,
|
|
221
240
|
# setsid/python os.setsid make this pid the new process group leader;
|
|
@@ -255,7 +274,13 @@ case "$CMD" in
|
|
|
255
274
|
emit_ok
|
|
256
275
|
;;
|
|
257
276
|
status)
|
|
258
|
-
if
|
|
277
|
+
if claude_smart_reflexio_url_is_remote; then
|
|
278
|
+
echo "remote configured at $REFLEXIO_URL"
|
|
279
|
+
elif is_our_backend_running; then
|
|
280
|
+
echo "running on http://localhost:$PORT"
|
|
281
|
+
else
|
|
282
|
+
echo "not running"
|
|
283
|
+
fi
|
|
259
284
|
;;
|
|
260
285
|
*)
|
|
261
286
|
emit_ok
|
package/plugin/scripts/cli.sh
CHANGED
|
@@ -15,6 +15,7 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
|
15
15
|
claude_smart_source_login_path
|
|
16
16
|
claude_smart_prepend_astral_bins
|
|
17
17
|
claude_smart_prepend_node_bins
|
|
18
|
+
claude_smart_source_reflexio_env
|
|
18
19
|
|
|
19
20
|
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
20
21
|
|
|
@@ -24,7 +25,7 @@ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
|
24
25
|
# on a broken install.
|
|
25
26
|
FAILURE_MARKER="$HOME/.claude-smart/install-failed"
|
|
26
27
|
if [ -f "$FAILURE_MARKER" ]; then
|
|
27
|
-
msg="$(
|
|
28
|
+
msg="$(head -n 1 "$FAILURE_MARKER" 2>/dev/null || echo "")"
|
|
28
29
|
[ -n "$msg" ] || msg="unknown error"
|
|
29
30
|
echo "claude-smart is not installed correctly: $msg" >&2
|
|
30
31
|
echo "Re-run the plugin's Setup (restart Claude Code) or fix the underlying issue and delete $FAILURE_MARKER to retry." >&2
|
|
@@ -50,7 +51,8 @@ if ! command -v uv >/dev/null 2>&1; then
|
|
|
50
51
|
fi
|
|
51
52
|
if ! command -v uv >/dev/null 2>&1; then
|
|
52
53
|
if [ -f "$FAILURE_MARKER" ]; then
|
|
53
|
-
msg="$(
|
|
54
|
+
msg="$(head -n 1 "$FAILURE_MARKER" 2>/dev/null || echo "")"
|
|
55
|
+
[ -n "$msg" ] || msg="unknown error"
|
|
54
56
|
echo "claude-smart: install failed: $msg" >&2
|
|
55
57
|
echo "Fix the underlying issue and delete $FAILURE_MARKER to retry." >&2
|
|
56
58
|
else
|
|
@@ -16,7 +16,46 @@ const DASHBOARD_PORT = 3001;
|
|
|
16
16
|
const LOG_MAX_BYTES = 10000000;
|
|
17
17
|
|
|
18
18
|
function emitOk() {
|
|
19
|
-
process.stdout.write('{"continue":true
|
|
19
|
+
process.stdout.write('{"continue":true}\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function emitHookOk() {
|
|
23
|
+
process.stdout.write('{"continue":true}\n');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function emitNormalizedHookOutput(stdout) {
|
|
27
|
+
const lines = String(stdout || "")
|
|
28
|
+
.split(/\r?\n/)
|
|
29
|
+
.map((line) => line.trim())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
if (lines.length === 0) {
|
|
32
|
+
emitHookOk();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const merged = {};
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(line);
|
|
41
|
+
} catch {
|
|
42
|
+
appendLog("backend.log", `[claude-smart] codex hook emitted non-JSON stdout: ${line.slice(0, 500)}`);
|
|
43
|
+
emitHookOk();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
47
|
+
appendLog("backend.log", `[claude-smart] codex hook emitted non-object JSON stdout: ${line.slice(0, 500)}`);
|
|
48
|
+
emitHookOk();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
Object.assign(merged, parsed);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!Object.prototype.hasOwnProperty.call(merged, "continue")) {
|
|
55
|
+
merged.continue = true;
|
|
56
|
+
}
|
|
57
|
+
delete merged.suppressOutput;
|
|
58
|
+
process.stdout.write(`${JSON.stringify(merged)}\n`);
|
|
20
59
|
}
|
|
21
60
|
|
|
22
61
|
function ensureDir(dir) {
|
|
@@ -121,6 +160,56 @@ function writeBackendUrl(port) {
|
|
|
121
160
|
fs.writeFileSync(backendUrlFile(), `http://localhost:${port}/\n`);
|
|
122
161
|
}
|
|
123
162
|
|
|
163
|
+
function unquoteEnvValue(value) {
|
|
164
|
+
const trimmed = String(value || "").trim();
|
|
165
|
+
if (trimmed.length >= 2) {
|
|
166
|
+
const first = trimmed[0];
|
|
167
|
+
const last = trimmed[trimmed.length - 1];
|
|
168
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
169
|
+
return trimmed.slice(1, -1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return trimmed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function loadReflexioEnv() {
|
|
176
|
+
const file = path.join(REFLEXIO_DIR, ".env");
|
|
177
|
+
let text;
|
|
178
|
+
try {
|
|
179
|
+
text = fs.readFileSync(file, "utf8");
|
|
180
|
+
} catch {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const allowed = new Set([
|
|
184
|
+
"REFLEXIO_URL",
|
|
185
|
+
"REFLEXIO_API_KEY",
|
|
186
|
+
"CLAUDE_SMART_USE_LOCAL_CLI",
|
|
187
|
+
"CLAUDE_SMART_USE_LOCAL_EMBEDDING",
|
|
188
|
+
"CLAUDE_SMART_BACKEND_AUTOSTART",
|
|
189
|
+
"CLAUDE_SMART_DASHBOARD_AUTOSTART",
|
|
190
|
+
"CLAUDE_SMART_CLI_PATH",
|
|
191
|
+
"CLAUDE_SMART_CLI_TIMEOUT",
|
|
192
|
+
"CLAUDE_SMART_STATE_DIR",
|
|
193
|
+
"CLAUDE_SMART_ENABLE_OPTIMIZER",
|
|
194
|
+
]);
|
|
195
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
196
|
+
let line = rawLine.trim();
|
|
197
|
+
if (!line || line.startsWith("#")) continue;
|
|
198
|
+
if (line.startsWith("export ")) line = line.slice("export ".length).trimStart();
|
|
199
|
+
const eq = line.indexOf("=");
|
|
200
|
+
if (eq < 0) continue;
|
|
201
|
+
const key = line.slice(0, eq).trim();
|
|
202
|
+
if (!allowed.has(key) || process.env[key]) continue;
|
|
203
|
+
process.env[key] = unquoteEnvValue(line.slice(eq + 1));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function reflexioUrlIsRemote() {
|
|
208
|
+
const url = process.env.REFLEXIO_URL || "";
|
|
209
|
+
if (!url) return false;
|
|
210
|
+
return !/^http:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?\/?$/i.test(url);
|
|
211
|
+
}
|
|
212
|
+
|
|
124
213
|
function codexCompatPath(root) {
|
|
125
214
|
const filename = process.platform === "win32"
|
|
126
215
|
? "codex-claude-compat.cmd"
|
|
@@ -294,6 +383,11 @@ async function startBackend(root) {
|
|
|
294
383
|
emitOk();
|
|
295
384
|
return;
|
|
296
385
|
}
|
|
386
|
+
if (reflexioUrlIsRemote()) {
|
|
387
|
+
appendLog("backend.log", "[claude-smart] backend: remote REFLEXIO_URL configured; skipping local backend start");
|
|
388
|
+
emitOk();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
297
391
|
const pidFile = path.join(STATE_DIR, "backend.pid");
|
|
298
392
|
for (const port of [DEFAULT_BACKEND_PORT, FALLBACK_BACKEND_PORT]) {
|
|
299
393
|
if (pidAlive(readPid(pidFile)) && await healthOk(port, "/health")) {
|
|
@@ -419,7 +513,7 @@ function runHook(root, event) {
|
|
|
419
513
|
}
|
|
420
514
|
if (!uv) {
|
|
421
515
|
appendLog("backend.log", "[claude-smart] hook: uv not on PATH after installer; skipping");
|
|
422
|
-
|
|
516
|
+
emitHookOk();
|
|
423
517
|
return 0;
|
|
424
518
|
}
|
|
425
519
|
}
|
|
@@ -434,17 +528,20 @@ function runHook(root, event) {
|
|
|
434
528
|
REFLEXIO_URL: readBackendUrl(),
|
|
435
529
|
CLAUDE_SMART_HOST: "codex",
|
|
436
530
|
CLAUDE_SMART_CLI_PATH: process.env.CLAUDE_SMART_CLI_PATH || codexCompatPath(root),
|
|
531
|
+
CLAUDE_SMART_CITATION_LINK_STYLE: process.env.CLAUDE_SMART_CITATION_LINK_STYLE || "osc8",
|
|
437
532
|
},
|
|
438
533
|
input,
|
|
439
|
-
stdio: ["pipe", "
|
|
534
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
440
535
|
windowsHide: true,
|
|
441
536
|
},
|
|
442
537
|
);
|
|
538
|
+
emitNormalizedHookOutput(result.stdout);
|
|
443
539
|
return typeof result.status === "number" ? result.status : 1;
|
|
444
540
|
}
|
|
445
541
|
|
|
446
542
|
async function main() {
|
|
447
543
|
prependRuntimePath();
|
|
544
|
+
loadReflexioEnv();
|
|
448
545
|
const root = pluginRoot();
|
|
449
546
|
process.env.PLUGIN_ROOT = root;
|
|
450
547
|
process.env.CLAUDE_PLUGIN_ROOT = root;
|
|
@@ -23,6 +23,7 @@ HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
|
23
23
|
. "$HERE/_lib.sh"
|
|
24
24
|
claude_smart_source_login_path
|
|
25
25
|
claude_smart_prepend_node_bins
|
|
26
|
+
claude_smart_source_reflexio_env
|
|
26
27
|
|
|
27
28
|
CMD="${1:-start}"
|
|
28
29
|
PORT=3001
|
|
@@ -36,7 +37,7 @@ PID_FILE="$STATE_DIR/dashboard.pid"
|
|
|
36
37
|
LOG_FILE="$STATE_DIR/dashboard.log"
|
|
37
38
|
mkdir -p "$STATE_DIR"
|
|
38
39
|
|
|
39
|
-
emit_ok() {
|
|
40
|
+
emit_ok() { claude_smart_emit_continue; }
|
|
40
41
|
|
|
41
42
|
# Tree-kill the recorded process. Delegates to claude_smart_kill_tree
|
|
42
43
|
# (POSIX: signal the process group; Windows: taskkill /T /F /PID).
|
|
@@ -45,15 +46,96 @@ kill_group() {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
# True if the marker header served by app/api/health is present on the
|
|
48
|
-
# port. Requires curl — absence is reported as false.
|
|
49
|
+
# port. Requires curl — absence is reported as false. This deliberately
|
|
50
|
+
# accepts any claude-smart dashboard, including stale cache versions, so
|
|
51
|
+
# stop/uninstall can safely reap them without touching foreign listeners.
|
|
49
52
|
marker_responds() {
|
|
50
53
|
command -v curl >/dev/null 2>&1 || return 1
|
|
51
54
|
curl -sfI "http://127.0.0.1:$PORT/api/health" 2>/dev/null \
|
|
52
55
|
| grep -qi '^x-claude-smart-dashboard:'
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
dashboard_health_headers() {
|
|
59
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
60
|
+
curl -sfI --connect-timeout 2 --max-time 5 "http://127.0.0.1:$PORT/api/health" 2>/dev/null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
header_value_from() {
|
|
64
|
+
header_name="$1"
|
|
65
|
+
awk -v wanted="$header_name" '
|
|
66
|
+
BEGIN { wanted = tolower(wanted) ":" }
|
|
67
|
+
{
|
|
68
|
+
line = $0
|
|
69
|
+
sub(/\r$/, "", line)
|
|
70
|
+
lower = tolower(line)
|
|
71
|
+
if (index(lower, wanted) == 1) {
|
|
72
|
+
sub(/^[^:]*:[[:space:]]*/, "", line)
|
|
73
|
+
print line
|
|
74
|
+
exit
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
canonical_dir() {
|
|
81
|
+
dir="$1"
|
|
82
|
+
(cd "$dir" 2>/dev/null && pwd -P) || printf '%s\n' "$dir"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
normalize_identity_path() {
|
|
86
|
+
path="$1"
|
|
87
|
+
if claude_smart_is_windows; then
|
|
88
|
+
if command -v cygpath >/dev/null 2>&1; then
|
|
89
|
+
path="$(cygpath -u "$path" 2>/dev/null || printf '%s\n' "$path")"
|
|
90
|
+
else
|
|
91
|
+
path="$(
|
|
92
|
+
printf '%s\n' "$path" | awk '{
|
|
93
|
+
gsub(/\\/, "/")
|
|
94
|
+
if ($0 ~ /^[A-Za-z]:/) {
|
|
95
|
+
drive = tolower(substr($0, 1, 1))
|
|
96
|
+
sub(/^[A-Za-z]:/, "/" drive)
|
|
97
|
+
}
|
|
98
|
+
print
|
|
99
|
+
}'
|
|
100
|
+
)"
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
while [ "${path%/}" != "$path" ] && [ "$path" != "/" ]; do
|
|
104
|
+
path="${path%/}"
|
|
105
|
+
done
|
|
106
|
+
printf '%s\n' "$path"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# True only if *this plugin root's* dashboard is on the port. The generic
|
|
110
|
+
# x-claude-smart-dashboard marker is intentionally not enough: after an
|
|
111
|
+
# install/update, an older cache can keep serving port 3001 until restarted.
|
|
112
|
+
dashboard_matches_current_root() {
|
|
113
|
+
expected_root="$(normalize_identity_path "$(canonical_dir "$PLUGIN_ROOT")")"
|
|
114
|
+
headers="$(dashboard_health_headers)" || return 1
|
|
115
|
+
printf '%s\n' "$headers" | grep -qi '^x-claude-smart-dashboard:' || return 1
|
|
116
|
+
actual_root="$(printf '%s\n' "$headers" | header_value_from "x-claude-smart-plugin-root")"
|
|
117
|
+
[ -n "$actual_root" ] || return 1
|
|
118
|
+
actual_root="$(normalize_identity_path "$actual_root")"
|
|
119
|
+
[ "$actual_root" = "$expected_root" ]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Kill a claude-smart dashboard listener currently holding the port. Gated by
|
|
123
|
+
# marker_responds so a foreign app on 3001 is never killed.
|
|
124
|
+
stop_dashboard_listener() {
|
|
125
|
+
marker_responds || return 0
|
|
126
|
+
command -v lsof >/dev/null 2>&1 || return 0
|
|
127
|
+
port_pid=$(lsof -t -i ":$PORT" -sTCP:LISTEN 2>/dev/null | head -n1)
|
|
128
|
+
[ -n "$port_pid" ] || return 0
|
|
129
|
+
kill -TERM "$port_pid" 2>/dev/null || true
|
|
130
|
+
for _ in 1 2 3 4 5; do
|
|
131
|
+
kill -0 "$port_pid" 2>/dev/null || return 0
|
|
132
|
+
sleep 0.2
|
|
133
|
+
done
|
|
134
|
+
kill -KILL "$port_pid" 2>/dev/null || true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# True only if *our* dashboard is on the port. Uses plugin-root identity so a
|
|
138
|
+
# stale claude-smart listener doesn't cause us to silently skip starting.
|
|
57
139
|
is_our_dashboard_running() {
|
|
58
140
|
if [ -f "$PID_FILE" ]; then
|
|
59
141
|
pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
|
|
@@ -62,7 +144,7 @@ is_our_dashboard_running() {
|
|
|
62
144
|
# don't claim "running" when the server crashed but the group leader
|
|
63
145
|
# lingered.
|
|
64
146
|
if command -v curl >/dev/null 2>&1; then
|
|
65
|
-
|
|
147
|
+
dashboard_matches_current_root && return 0
|
|
66
148
|
else
|
|
67
149
|
# No curl — fall back to PID liveness alone.
|
|
68
150
|
return 0
|
|
@@ -71,7 +153,7 @@ is_our_dashboard_running() {
|
|
|
71
153
|
fi
|
|
72
154
|
# No PID or dead PID — probe the port for our marker (recovers after a
|
|
73
155
|
# stale PID file from a crash).
|
|
74
|
-
|
|
156
|
+
dashboard_matches_current_root && return 0
|
|
75
157
|
return 1
|
|
76
158
|
}
|
|
77
159
|
|
|
@@ -87,6 +169,9 @@ port_occupied() {
|
|
|
87
169
|
|
|
88
170
|
case "$CMD" in
|
|
89
171
|
start)
|
|
172
|
+
if claude_smart_is_internal_invocation_env; then
|
|
173
|
+
emit_ok; exit 0
|
|
174
|
+
fi
|
|
90
175
|
# Opt-out: users who don't want the dashboard long-lived can set
|
|
91
176
|
# CLAUDE_SMART_DASHBOARD_AUTOSTART=0 in their environment.
|
|
92
177
|
if [ "${CLAUDE_SMART_DASHBOARD_AUTOSTART:-1}" = "0" ]; then
|
|
@@ -94,6 +179,10 @@ case "$CMD" in
|
|
|
94
179
|
fi
|
|
95
180
|
if [ ! -d "$DASHBOARD_DIR" ]; then emit_ok; exit 0; fi
|
|
96
181
|
if is_our_dashboard_running; then claude_smart_clear_dashboard_unavailable; emit_ok; exit 0; fi
|
|
182
|
+
if marker_responds; then
|
|
183
|
+
echo "[claude-smart] dashboard: stale claude-smart dashboard on port $PORT; restarting from $PLUGIN_ROOT" >>"$LOG_FILE"
|
|
184
|
+
stop_dashboard_listener
|
|
185
|
+
fi
|
|
97
186
|
if port_occupied; then
|
|
98
187
|
echo "[claude-smart] dashboard: port $PORT held by another process; skipping" >>"$LOG_FILE"
|
|
99
188
|
emit_ok; exit 0
|
|
@@ -149,7 +238,7 @@ case "$CMD" in
|
|
|
149
238
|
echo "$dash_pid" > "$PID_FILE"
|
|
150
239
|
dashboard_ready=0
|
|
151
240
|
for _ in 1 2 3 4 5; do
|
|
152
|
-
if
|
|
241
|
+
if dashboard_matches_current_root; then
|
|
153
242
|
dashboard_ready=1
|
|
154
243
|
claude_smart_clear_dashboard_unavailable
|
|
155
244
|
break
|
|
@@ -166,21 +255,11 @@ case "$CMD" in
|
|
|
166
255
|
kill_group "$(cat "$PID_FILE" 2>/dev/null)"
|
|
167
256
|
rm -f "$PID_FILE"
|
|
168
257
|
fi
|
|
169
|
-
# Fallback: if
|
|
258
|
+
# Fallback: if a claude-smart dashboard is still responding on the port (e.g.,
|
|
170
259
|
# was started outside this script, or the PGID kill missed because
|
|
171
260
|
# the process wasn't the group leader) kill whoever owns the port.
|
|
172
261
|
# Gated on the marker header so we never touch a foreign listener.
|
|
173
|
-
|
|
174
|
-
port_pid=$(lsof -t -i ":$PORT" -sTCP:LISTEN 2>/dev/null | head -n1)
|
|
175
|
-
if [ -n "$port_pid" ]; then
|
|
176
|
-
kill -TERM "$port_pid" 2>/dev/null || true
|
|
177
|
-
for _ in 1 2 3 4 5; do
|
|
178
|
-
kill -0 "$port_pid" 2>/dev/null || break
|
|
179
|
-
sleep 0.2
|
|
180
|
-
done
|
|
181
|
-
kill -KILL "$port_pid" 2>/dev/null || true
|
|
182
|
-
fi
|
|
183
|
-
fi
|
|
262
|
+
stop_dashboard_listener
|
|
184
263
|
emit_ok
|
|
185
264
|
;;
|
|
186
265
|
session-end)
|
|
@@ -18,14 +18,23 @@ case "$EVENT" in
|
|
|
18
18
|
EVENT="${2:-}"
|
|
19
19
|
;;
|
|
20
20
|
esac
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
export CLAUDE_SMART_HOST="$HOST"
|
|
22
|
+
if [ "$HOST" = "codex" ] && [ -z "${CLAUDE_SMART_CITATION_LINK_STYLE:-}" ]; then
|
|
23
|
+
export CLAUDE_SMART_CITATION_LINK_STYLE="osc8"
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
26
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
27
27
|
# shellcheck source=_lib.sh
|
|
28
28
|
. "$HERE/_lib.sh"
|
|
29
|
+
if [ -z "$EVENT" ]; then
|
|
30
|
+
claude_smart_emit_continue
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if claude_smart_is_internal_invocation_env; then
|
|
35
|
+
claude_smart_emit_continue
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
29
38
|
# Pick up uv from the user's login-shell PATH (covers ~/.local/bin populated
|
|
30
39
|
# by the astral.sh installer) so a fresh install works before the user
|
|
31
40
|
# restarts their terminal. Matches the pattern used by smart-install.sh.
|
|
@@ -33,16 +42,27 @@ claude_smart_source_login_path
|
|
|
33
42
|
# Explicit fallback for the astral.sh installer's default paths, in case
|
|
34
43
|
# the user's login-shell rc hasn't yet been re-sourced to pick them up.
|
|
35
44
|
claude_smart_prepend_astral_bins
|
|
45
|
+
claude_smart_source_reflexio_env
|
|
36
46
|
|
|
37
47
|
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
38
48
|
|
|
39
49
|
FAILURE_MARKER="$HOME/.claude-smart/install-failed"
|
|
40
50
|
STATE_DIR="$HOME/.claude-smart"
|
|
41
51
|
if [ -f "$FAILURE_MARKER" ]; then
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
# Auto-clear the marker when install inputs have changed since the
|
|
53
|
+
# failure was recorded (pyproject.toml / uv.lock / smart-install.sh /
|
|
54
|
+
# python interpreter etc.). Without this the plugin stays muted forever
|
|
55
|
+
# after a transient sync failure even when the user has fixed it.
|
|
56
|
+
stored_fp="$(awk -F= '$1=="fingerprint"{print $2; exit}' "$FAILURE_MARKER" 2>/dev/null || true)"
|
|
57
|
+
current_fp="$(claude_smart_install_fingerprint_hash "$PLUGIN_ROOT" "$HERE" 2>/dev/null || true)"
|
|
58
|
+
if [ -z "$stored_fp" ] || [ "$stored_fp" != "$current_fp" ]; then
|
|
59
|
+
rm -f "$FAILURE_MARKER"
|
|
60
|
+
else
|
|
61
|
+
if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then
|
|
62
|
+
python3 - "$FAILURE_MARKER" <<'PY'
|
|
44
63
|
import json, pathlib, sys
|
|
45
|
-
|
|
64
|
+
first = pathlib.Path(sys.argv[1]).read_text().splitlines()
|
|
65
|
+
msg = (first[0].strip() if first else "") or "unknown error"
|
|
46
66
|
print(json.dumps({
|
|
47
67
|
"hookSpecificOutput": {
|
|
48
68
|
"hookEventName": "SessionStart",
|
|
@@ -55,10 +75,11 @@ print(json.dumps({
|
|
|
55
75
|
}
|
|
56
76
|
}))
|
|
57
77
|
PY
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
else
|
|
79
|
+
claude_smart_emit_continue
|
|
80
|
+
fi
|
|
81
|
+
exit 0
|
|
60
82
|
fi
|
|
61
|
-
exit 0
|
|
62
83
|
fi
|
|
63
84
|
|
|
64
85
|
if ! command -v uv >/dev/null 2>&1; then
|
|
@@ -67,7 +88,7 @@ if ! command -v uv >/dev/null 2>&1; then
|
|
|
67
88
|
# the same installer detached so normal work is not blocked by first-run
|
|
68
89
|
# dependency setup.
|
|
69
90
|
if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then
|
|
70
|
-
|
|
91
|
+
claude_smart_emit_continue
|
|
71
92
|
exit 0
|
|
72
93
|
fi
|
|
73
94
|
if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
|
|
@@ -87,7 +108,7 @@ if ! command -v uv >/dev/null 2>&1; then
|
|
|
87
108
|
fi
|
|
88
109
|
fi
|
|
89
110
|
if ! command -v uv >/dev/null 2>&1; then
|
|
90
|
-
|
|
111
|
+
claude_smart_emit_continue
|
|
91
112
|
exit 0
|
|
92
113
|
fi
|
|
93
114
|
fi
|