claude-smart 0.2.23 → 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,208 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Auto-start the reflexio FastAPI backend (port 8071) if it's not already
|
|
3
|
+
# running. Mirrors dashboard-service.sh: detached spawn, returns immediately
|
|
4
|
+
# so the SessionStart hook doesn't block the session.
|
|
5
|
+
#
|
|
6
|
+
# Subcommands:
|
|
7
|
+
# start probe /health; if nothing we recognize is on the port,
|
|
8
|
+
# spawn `uv run reflexio services start --only backend
|
|
9
|
+
# --no-reload` detached. Polls /health briefly so first
|
|
10
|
+
# use after session start lands on a warm server, then
|
|
11
|
+
# returns a continue payload regardless.
|
|
12
|
+
# stop SIGTERM the recorded process group, escalating to
|
|
13
|
+
# SIGKILL after a short grace period.
|
|
14
|
+
# session-end no-op by default; only stops the backend if
|
|
15
|
+
# CLAUDE_SMART_BACKEND_STOP_ON_END=1 (opt-in — the
|
|
16
|
+
# backend is intended to be long-lived across sessions).
|
|
17
|
+
# status print "running on http://localhost:PORT" or "not running".
|
|
18
|
+
set -eu
|
|
19
|
+
|
|
20
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
+
# shellcheck source=_lib.sh
|
|
22
|
+
. "$HERE/_lib.sh"
|
|
23
|
+
claude_smart_source_login_path
|
|
24
|
+
claude_smart_prepend_astral_bins
|
|
25
|
+
|
|
26
|
+
CMD="${1:-start}"
|
|
27
|
+
PORT=8071
|
|
28
|
+
# Pass through to `reflexio services start/stop` so the spawned backend
|
|
29
|
+
# binds to PORT instead of reflexio's library default (8081).
|
|
30
|
+
export BACKEND_PORT="$PORT"
|
|
31
|
+
|
|
32
|
+
# Default: route extraction through the local claude CLI + ONNX embedder
|
|
33
|
+
# so claude-smart works without any LLM API key. Users can opt out by
|
|
34
|
+
# pre-exporting these to 0.
|
|
35
|
+
export CLAUDE_SMART_USE_LOCAL_CLI="${CLAUDE_SMART_USE_LOCAL_CLI:-1}"
|
|
36
|
+
export CLAUDE_SMART_USE_LOCAL_EMBEDDING="${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}"
|
|
37
|
+
# The backend can be spawned from contexts whose PATH lacks the claude
|
|
38
|
+
# CLI dir (commonly ~/.local/bin or /opt/homebrew/bin). Pin the CLI
|
|
39
|
+
# explicitly if we can resolve it from our own (post-login-path) PATH.
|
|
40
|
+
if [ -z "${CLAUDE_SMART_CLI_PATH:-}" ]; then
|
|
41
|
+
if _cs_claude_path=$(command -v claude 2>/dev/null) && [ -n "$_cs_claude_path" ]; then
|
|
42
|
+
export CLAUDE_SMART_CLI_PATH="$_cs_claude_path"
|
|
43
|
+
elif [ -x "$HOME/.local/bin/claude" ]; then
|
|
44
|
+
export CLAUDE_SMART_CLI_PATH="$HOME/.local/bin/claude"
|
|
45
|
+
fi
|
|
46
|
+
unset _cs_claude_path
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
50
|
+
|
|
51
|
+
STATE_DIR="$HOME/.claude-smart"
|
|
52
|
+
PID_FILE="$STATE_DIR/backend.pid"
|
|
53
|
+
LOG_FILE="$STATE_DIR/backend.log"
|
|
54
|
+
mkdir -p "$STATE_DIR"
|
|
55
|
+
|
|
56
|
+
emit_ok() { echo '{"continue":true,"suppressOutput":true}'; }
|
|
57
|
+
|
|
58
|
+
# Tree-kill the recorded process. Delegates to claude_smart_kill_tree
|
|
59
|
+
# (POSIX: signal the process group; Windows: taskkill /T /F /PID).
|
|
60
|
+
kill_group() {
|
|
61
|
+
claude_smart_kill_tree "$1"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# True if /health returns 200. Reflexio's /health is a plain GET with no
|
|
65
|
+
# marker header, so we can't distinguish our backend from someone else's
|
|
66
|
+
# reflexio on the same port — if you run two reflexio instances on 8071
|
|
67
|
+
# you'll get collision regardless of what we do here.
|
|
68
|
+
backend_healthy() {
|
|
69
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
70
|
+
curl -sf -o /dev/null "http://127.0.0.1:$PORT/health" 2>/dev/null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# True only if the recorded PID is alive AND /health responds. A stale
|
|
74
|
+
# PID file from a crashed backend is not enough — we must see the port
|
|
75
|
+
# actually answer, so next hook retries cleanly.
|
|
76
|
+
is_our_backend_running() {
|
|
77
|
+
if [ -f "$PID_FILE" ]; then
|
|
78
|
+
pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
|
|
79
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
80
|
+
backend_healthy && return 0
|
|
81
|
+
fi
|
|
82
|
+
fi
|
|
83
|
+
# Recover from a missing PID file if a foreign-but-functional reflexio
|
|
84
|
+
# is already serving — no need to start a second one.
|
|
85
|
+
backend_healthy && return 0
|
|
86
|
+
return 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# True if *anything* is listening on the port (even non-HTTP). Used to
|
|
90
|
+
# avoid stomping on a foreign listener with a failed-to-start uvicorn.
|
|
91
|
+
port_occupied() {
|
|
92
|
+
(echo >"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Reap any reflexio/uvicorn listener still holding $PORT after the PID
|
|
96
|
+
# file kill. Filters by cmdline so we don't knock over an unrelated
|
|
97
|
+
# service a user has bound to 8071 — symmetric with start's refusal to
|
|
98
|
+
# stomp on a foreign listener. Silent on failure.
|
|
99
|
+
reap_port_listeners() {
|
|
100
|
+
command -v lsof >/dev/null 2>&1 || return 0
|
|
101
|
+
candidates=$(lsof -ti:"$PORT" 2>/dev/null) || candidates=""
|
|
102
|
+
[ -z "$candidates" ] && return 0
|
|
103
|
+
ours=""
|
|
104
|
+
for pid in $candidates; do
|
|
105
|
+
cmdline=$(ps -p "$pid" -o command= 2>/dev/null || true)
|
|
106
|
+
case "$cmdline" in
|
|
107
|
+
*reflexio*|*uvicorn*) ours="$ours $pid" ;;
|
|
108
|
+
esac
|
|
109
|
+
done
|
|
110
|
+
[ -z "$ours" ] && return 0
|
|
111
|
+
# shellcheck disable=SC2086
|
|
112
|
+
kill -TERM $ours 2>/dev/null || true
|
|
113
|
+
sleep 1
|
|
114
|
+
remaining=""
|
|
115
|
+
for pid in $ours; do
|
|
116
|
+
kill -0 "$pid" 2>/dev/null && remaining="$remaining $pid"
|
|
117
|
+
done
|
|
118
|
+
[ -z "$remaining" ] && return 0
|
|
119
|
+
# shellcheck disable=SC2086
|
|
120
|
+
kill -KILL $remaining 2>/dev/null || true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Full shutdown: kill the recorded process group (if any) then sweep the
|
|
124
|
+
# port for surviving reflexio listeners. Used by both `stop` and the
|
|
125
|
+
# opt-in `session-end` path so a stale/missing PID file doesn't produce
|
|
126
|
+
# a silent no-op.
|
|
127
|
+
full_stop() {
|
|
128
|
+
if [ -f "$PID_FILE" ]; then
|
|
129
|
+
kill_group "$(cat "$PID_FILE" 2>/dev/null)"
|
|
130
|
+
rm -f "$PID_FILE"
|
|
131
|
+
fi
|
|
132
|
+
reap_port_listeners
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case "$CMD" in
|
|
136
|
+
start)
|
|
137
|
+
# Opt-out: users who don't want the backend managed by the hook can
|
|
138
|
+
# set CLAUDE_SMART_BACKEND_AUTOSTART=0.
|
|
139
|
+
if [ "${CLAUDE_SMART_BACKEND_AUTOSTART:-1}" = "0" ]; then
|
|
140
|
+
emit_ok; exit 0
|
|
141
|
+
fi
|
|
142
|
+
if is_our_backend_running; then emit_ok; exit 0; fi
|
|
143
|
+
if port_occupied; then
|
|
144
|
+
# Something answered the TCP probe but /health didn't — don't
|
|
145
|
+
# start a second uvicorn on top of it.
|
|
146
|
+
echo "[claude-smart] backend: port $PORT held by another process; skipping" >>"$LOG_FILE"
|
|
147
|
+
emit_ok; exit 0
|
|
148
|
+
fi
|
|
149
|
+
if ! command -v uv >/dev/null 2>&1; then
|
|
150
|
+
echo "[claude-smart] backend: uv not on PATH; skipping" >>"$LOG_FILE"
|
|
151
|
+
emit_ok; exit 0
|
|
152
|
+
fi
|
|
153
|
+
cd "$PLUGIN_ROOT"
|
|
154
|
+
|
|
155
|
+
# Cap local interaction history to keep the SQLite store small for
|
|
156
|
+
# claude-smart users. Reflexio's library defaults are much higher
|
|
157
|
+
# (250k/50k) for server deployments; here we override only in the
|
|
158
|
+
# claude-smart plugin context. Users can still override via env.
|
|
159
|
+
export INTERACTION_CLEANUP_THRESHOLD="${INTERACTION_CLEANUP_THRESHOLD:-500}"
|
|
160
|
+
export INTERACTION_CLEANUP_DELETE_COUNT="${INTERACTION_CLEANUP_DELETE_COUNT:-200}"
|
|
161
|
+
|
|
162
|
+
# --no-reload: uvicorn's reloader forks a supervisor; makes
|
|
163
|
+
# bookkeeping harder and we don't need hot-reload for a user-facing
|
|
164
|
+
# service. Detach via claude_smart_spawn_detached so the same code
|
|
165
|
+
# path covers Linux (setsid), macOS (python3 os.setsid), and Windows
|
|
166
|
+
# (nohup; no process groups). Caller-side stdout/stderr redirection
|
|
167
|
+
# works across all three primitives — Git Bash routes the > and 2>&1
|
|
168
|
+
# through to the underlying CRT before nohup execs the child.
|
|
169
|
+
claude_smart_spawn_detached uv run --project "$PLUGIN_ROOT" --quiet \
|
|
170
|
+
reflexio services start --only backend --no-reload \
|
|
171
|
+
>>"$LOG_FILE" 2>&1
|
|
172
|
+
svc_pid=$!
|
|
173
|
+
# Record the spawned pid, not a pgid sampled with ps. On POSIX,
|
|
174
|
+
# setsid/python os.setsid make this pid the new process group leader;
|
|
175
|
+
# sampling immediately can race and capture the caller's pgid instead.
|
|
176
|
+
# On Windows, claude_smart_kill_tree translates the MSYS pid to WINPID.
|
|
177
|
+
echo "$svc_pid" > "$PID_FILE"
|
|
178
|
+
|
|
179
|
+
# Give uvicorn up to ~10s to answer /health. The very first boot
|
|
180
|
+
# after a fresh checkout may be slower (LiteLLM import, chromadb
|
|
181
|
+
# warmup) — dashboard auto-start does the same thing. We always
|
|
182
|
+
# return ok; the backend catches up in background if it needs to.
|
|
183
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
184
|
+
backend_healthy && break
|
|
185
|
+
sleep 1
|
|
186
|
+
done
|
|
187
|
+
emit_ok
|
|
188
|
+
;;
|
|
189
|
+
stop)
|
|
190
|
+
full_stop
|
|
191
|
+
emit_ok
|
|
192
|
+
;;
|
|
193
|
+
session-end)
|
|
194
|
+
# Default: leave the backend running so learning keeps flowing
|
|
195
|
+
# between sessions. Opt in to teardown with
|
|
196
|
+
# CLAUDE_SMART_BACKEND_STOP_ON_END=1.
|
|
197
|
+
if [ "${CLAUDE_SMART_BACKEND_STOP_ON_END:-0}" = "1" ]; then
|
|
198
|
+
full_stop
|
|
199
|
+
fi
|
|
200
|
+
emit_ok
|
|
201
|
+
;;
|
|
202
|
+
status)
|
|
203
|
+
if is_our_backend_running; then echo "running on http://localhost:$PORT"; else echo "not running"; fi
|
|
204
|
+
;;
|
|
205
|
+
*)
|
|
206
|
+
emit_ok
|
|
207
|
+
;;
|
|
208
|
+
esac
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Wrapper for slash commands that invoke the claude_smart CLI via uv.
|
|
3
|
+
# Claude Code runs `!` bash directives in slash command .md files in a
|
|
4
|
+
# non-interactive, non-login shell that does NOT source ~/.zshrc or
|
|
5
|
+
# ~/.bash_profile. As a result, binaries installed by smart-install.sh
|
|
6
|
+
# at ~/.local/bin (e.g. uv from the astral.sh installer) are invisible
|
|
7
|
+
# to those directives until the user manually re-sources their shell rc.
|
|
8
|
+
# This wrapper bootstraps PATH the same way hook_entry.sh does so the
|
|
9
|
+
# slash commands work on a fresh install.
|
|
10
|
+
set -eu
|
|
11
|
+
|
|
12
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
# shellcheck source=_lib.sh
|
|
14
|
+
. "$HERE/_lib.sh"
|
|
15
|
+
claude_smart_source_login_path
|
|
16
|
+
claude_smart_prepend_astral_bins
|
|
17
|
+
claude_smart_prepend_node_bins
|
|
18
|
+
|
|
19
|
+
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
20
|
+
|
|
21
|
+
# If the Setup hook recorded an install failure, surface that reason
|
|
22
|
+
# instead of falling through to a generic "uv not found" — mirrors the
|
|
23
|
+
# branch at hook_entry.sh so slash commands and hooks behave consistently
|
|
24
|
+
# on a broken install.
|
|
25
|
+
FAILURE_MARKER="$HOME/.claude-smart/install-failed"
|
|
26
|
+
if [ -f "$FAILURE_MARKER" ]; then
|
|
27
|
+
msg="$(cat "$FAILURE_MARKER" 2>/dev/null || echo "")"
|
|
28
|
+
[ -n "$msg" ] || msg="unknown error"
|
|
29
|
+
echo "claude-smart is not installed correctly: $msg" >&2
|
|
30
|
+
echo "Re-run the plugin's Setup (restart Claude Code) or fix the underlying issue and delete $FAILURE_MARKER to retry." >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if ! command -v uv >/dev/null 2>&1; then
|
|
35
|
+
echo "claude-smart: 'uv' not found on PATH." >&2
|
|
36
|
+
echo "Install it from https://docs.astral.sh/uv/ or restart Claude Code so the Setup hook can install it." >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
exec uv run --project "$PLUGIN_ROOT" --quiet python -m claude_smart.cli "$@"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build the Next.js dashboard ($PLUGIN_ROOT/dashboard) in two steps:
|
|
3
|
+
# 1. npm ci (skipped if node_modules exists and is newer than package.json
|
|
4
|
+
# and package-lock.json)
|
|
5
|
+
# 2. npm run build (skipped if .next exists and is newer than
|
|
6
|
+
# package.json)
|
|
7
|
+
#
|
|
8
|
+
# Designed to run detached from any hook so the multi-minute first-run
|
|
9
|
+
# cost never trips Claude Code's hook timeout. dashboard-service.sh
|
|
10
|
+
# spawns this on first SessionStart when .next is missing; the user can
|
|
11
|
+
# also invoke it manually for recovery.
|
|
12
|
+
#
|
|
13
|
+
# Concurrency: a build-pid file at $STATE_DIR/dashboard-build.pid is
|
|
14
|
+
# used to mark "build in progress" so dashboard-open.sh can surface a
|
|
15
|
+
# "still building, retry in ~1 minute" message instead of the generic
|
|
16
|
+
# .next-missing error. The pid file is removed on exit (success, fail,
|
|
17
|
+
# or interrupt).
|
|
18
|
+
#
|
|
19
|
+
# Partial-build safety: an INT/TERM trap wipes any half-written .next
|
|
20
|
+
# so dashboard-service.sh's "no .next → start build" probe stays honest.
|
|
21
|
+
set -eu
|
|
22
|
+
|
|
23
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
24
|
+
# shellcheck source=_lib.sh
|
|
25
|
+
. "$HERE/_lib.sh"
|
|
26
|
+
claude_smart_source_login_path
|
|
27
|
+
claude_smart_prepend_node_bins
|
|
28
|
+
|
|
29
|
+
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
30
|
+
DASHBOARD_DIR="$PLUGIN_ROOT/dashboard"
|
|
31
|
+
|
|
32
|
+
STATE_DIR="$HOME/.claude-smart"
|
|
33
|
+
LOG_FILE="$STATE_DIR/dashboard.log"
|
|
34
|
+
BUILD_PID_FILE="$STATE_DIR/dashboard-build.pid"
|
|
35
|
+
BUILD_LOCK_DIR="$STATE_DIR/dashboard-build.lock"
|
|
36
|
+
mkdir -p "$STATE_DIR"
|
|
37
|
+
|
|
38
|
+
log() { printf '[claude-smart] %s\n' "$1" >>"$LOG_FILE"; }
|
|
39
|
+
|
|
40
|
+
if [ ! -d "$DASHBOARD_DIR" ]; then
|
|
41
|
+
log "dashboard build: no $DASHBOARD_DIR; nothing to do"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
NPM_BIN=$(claude_smart_resolve_npm || true)
|
|
45
|
+
if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
|
|
46
|
+
reason="npm is not on PATH; dashboard dependencies cannot be installed"
|
|
47
|
+
log "dashboard build: $reason"
|
|
48
|
+
claude_smart_write_dashboard_unavailable "$reason"
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Atomic single-flight: mkdir is a single atomic syscall, so two concurrent
|
|
53
|
+
# builds (e.g., smart-install.sh and a SessionStart-driven dashboard-service.sh
|
|
54
|
+
# firing within milliseconds on first install) cannot both pass this check.
|
|
55
|
+
# BUILD_PID_FILE remains as status metadata for dashboard-open.sh and
|
|
56
|
+
# dashboard-service.sh to probe — it is written only after the lock is held
|
|
57
|
+
# and removed only by the lock holder.
|
|
58
|
+
if ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; then
|
|
59
|
+
if claude_smart_pid_alive_file "$BUILD_PID_FILE"; then
|
|
60
|
+
log "dashboard build: already in progress; skipping"
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
# Stale lock from a crashed build (lock dir survived but owner is gone).
|
|
64
|
+
# Reclaim it; if another process beats us to the reclaim, defer to them.
|
|
65
|
+
rm -rf "$BUILD_LOCK_DIR"
|
|
66
|
+
rm -f "$BUILD_PID_FILE"
|
|
67
|
+
if ! mkdir "$BUILD_LOCK_DIR" 2>/dev/null; then
|
|
68
|
+
log "dashboard build: lost race for stale lock; skipping"
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
echo $$ > "$BUILD_PID_FILE"
|
|
73
|
+
|
|
74
|
+
release_lock() {
|
|
75
|
+
rm -f "$BUILD_PID_FILE"
|
|
76
|
+
rmdir "$BUILD_LOCK_DIR" 2>/dev/null || rm -rf "$BUILD_LOCK_DIR"
|
|
77
|
+
}
|
|
78
|
+
cleanup() {
|
|
79
|
+
status=$?
|
|
80
|
+
release_lock
|
|
81
|
+
exit "${status:-0}"
|
|
82
|
+
}
|
|
83
|
+
on_interrupt() {
|
|
84
|
+
rm -rf "$DASHBOARD_DIR/.next"
|
|
85
|
+
rm -rf "$DASHBOARD_DIR/node_modules"
|
|
86
|
+
release_lock
|
|
87
|
+
log "dashboard build: interrupted; removed partial .next and node_modules"
|
|
88
|
+
exit 130
|
|
89
|
+
}
|
|
90
|
+
trap cleanup EXIT
|
|
91
|
+
trap on_interrupt INT TERM
|
|
92
|
+
|
|
93
|
+
cd "$DASHBOARD_DIR"
|
|
94
|
+
|
|
95
|
+
# Cheap freshness check: skip reinstall when node_modules is newer than
|
|
96
|
+
# package.json and package-lock.json. Avoids re-downloading the dep tree
|
|
97
|
+
# on every SessionStart while still picking up version bumps when the
|
|
98
|
+
# plugin updates.
|
|
99
|
+
needs_install=1
|
|
100
|
+
if [ -d node_modules ] && [ node_modules -nt package.json ]; then
|
|
101
|
+
if [ ! -f package-lock.json ] || [ node_modules -nt package-lock.json ]; then
|
|
102
|
+
needs_install=0
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
if [ "$needs_install" = "1" ]; then
|
|
106
|
+
if [ -f package-lock.json ]; then
|
|
107
|
+
install_cmd="ci"
|
|
108
|
+
else
|
|
109
|
+
install_cmd="install"
|
|
110
|
+
fi
|
|
111
|
+
log "dashboard build: running npm $install_cmd..."
|
|
112
|
+
if ! "$NPM_BIN" "$install_cmd" --silent --no-fund --no-audit >>"$LOG_FILE" 2>&1; then
|
|
113
|
+
rm -rf "$DASHBOARD_DIR/node_modules"
|
|
114
|
+
reason="npm $install_cmd failed; removed partial node_modules; see $LOG_FILE"
|
|
115
|
+
log "dashboard build: $reason"
|
|
116
|
+
claude_smart_write_dashboard_unavailable "$reason"
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
needs_build=1
|
|
122
|
+
if [ -d .next ] && [ .next -nt package.json ]; then
|
|
123
|
+
needs_build=0
|
|
124
|
+
fi
|
|
125
|
+
if [ "$needs_build" = "1" ]; then
|
|
126
|
+
log "dashboard build: running next build (this can take 1-2 min)..."
|
|
127
|
+
if ! "$NPM_BIN" run build >>"$LOG_FILE" 2>&1; then
|
|
128
|
+
rm -rf "$DASHBOARD_DIR/.next"
|
|
129
|
+
reason="next build failed; see $LOG_FILE"
|
|
130
|
+
log "dashboard build: $reason"
|
|
131
|
+
claude_smart_write_dashboard_unavailable "$reason"
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
claude_smart_clear_dashboard_unavailable
|
|
135
|
+
log "dashboard build: complete"
|
|
136
|
+
else
|
|
137
|
+
claude_smart_clear_dashboard_unavailable
|
|
138
|
+
log "dashboard build: .next is up-to-date; skipping"
|
|
139
|
+
fi
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Start backend + dashboard (idempotent), wait briefly for dashboard to come
|
|
3
|
+
# up, print statuses, then open http://localhost:3001 in the default browser.
|
|
4
|
+
#
|
|
5
|
+
# Exists so the /claude-smart:dashboard slash command can invoke a single
|
|
6
|
+
# plain `bash <script>` with no inline $(...) or ${...:-...} expansion in
|
|
7
|
+
# the command string — Claude Code's permission checker rejects those.
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
SCRIPTS="$HERE"
|
|
12
|
+
# shellcheck source=_lib.sh
|
|
13
|
+
. "$HERE/_lib.sh"
|
|
14
|
+
claude_smart_source_login_path
|
|
15
|
+
claude_smart_prepend_node_bins
|
|
16
|
+
|
|
17
|
+
STATE_DIR="$HOME/.claude-smart"
|
|
18
|
+
BACKEND_LOG="$STATE_DIR/backend.log"
|
|
19
|
+
DASHBOARD_LOG="$STATE_DIR/dashboard.log"
|
|
20
|
+
DASHBOARD_UNAVAILABLE="$(claude_smart_dashboard_unavailable_marker)"
|
|
21
|
+
|
|
22
|
+
# Capture start-command output so we can surface fatal errors (e.g. uv/npm
|
|
23
|
+
# missing, port collisions, .next not built) rather than silently swallow
|
|
24
|
+
# them. The service scripts themselves write to their own log files on
|
|
25
|
+
# successful detached spawn, so stdout/stderr here are normally empty.
|
|
26
|
+
backend_start_out=$(bash "$SCRIPTS/backend-service.sh" start 2>&1) || true
|
|
27
|
+
dashboard_start_out=$(bash "$SCRIPTS/dashboard-service.sh" start 2>&1) || true
|
|
28
|
+
|
|
29
|
+
# Poll both services for up to ~10s so a cold boot has time to come up.
|
|
30
|
+
backend_status="not running"
|
|
31
|
+
dashboard_status="not running"
|
|
32
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
33
|
+
backend_status=$(bash "$SCRIPTS/backend-service.sh" status)
|
|
34
|
+
dashboard_status=$(bash "$SCRIPTS/dashboard-service.sh" status)
|
|
35
|
+
if [ "$backend_status" != "not running" ] && [ "$dashboard_status" != "not running" ]; then
|
|
36
|
+
break
|
|
37
|
+
fi
|
|
38
|
+
sleep 1
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
echo "backend: $backend_status"
|
|
42
|
+
echo "dashboard: $dashboard_status"
|
|
43
|
+
|
|
44
|
+
# Print any "skipping" diagnostic the service scripts appended to their
|
|
45
|
+
# logs (uv/npm missing, port held, .next missing, etc.). Tail is cheap
|
|
46
|
+
# and gives the user something actionable instead of just "not running".
|
|
47
|
+
show_log_tail() {
|
|
48
|
+
label="$1"
|
|
49
|
+
log_path="$2"
|
|
50
|
+
if [ -f "$log_path" ]; then
|
|
51
|
+
tail=$(tail -n 20 "$log_path" 2>/dev/null || true)
|
|
52
|
+
if [ -n "$tail" ]; then
|
|
53
|
+
echo ""
|
|
54
|
+
echo "--- $label log (last 20 lines: $log_path) ---"
|
|
55
|
+
echo "$tail"
|
|
56
|
+
fi
|
|
57
|
+
else
|
|
58
|
+
echo ""
|
|
59
|
+
echo "[$label] no log at $log_path"
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
failed=0
|
|
64
|
+
if [ "$backend_status" = "not running" ]; then
|
|
65
|
+
failed=1
|
|
66
|
+
echo ""
|
|
67
|
+
echo "ERROR: backend failed to start on http://localhost:8071"
|
|
68
|
+
[ -n "$backend_start_out" ] && echo "$backend_start_out"
|
|
69
|
+
show_log_tail "backend" "$BACKEND_LOG"
|
|
70
|
+
fi
|
|
71
|
+
if [ "$dashboard_status" = "not running" ]; then
|
|
72
|
+
failed=1
|
|
73
|
+
echo ""
|
|
74
|
+
BUILD_PID_FILE="$STATE_DIR/dashboard-build.pid"
|
|
75
|
+
if claude_smart_pid_alive_file "$BUILD_PID_FILE"; then
|
|
76
|
+
echo "dashboard: still building (first-run cost, ~1-2 min). Re-run /claude-smart:dashboard in a minute."
|
|
77
|
+
else
|
|
78
|
+
echo "ERROR: dashboard failed to start on http://localhost:3001"
|
|
79
|
+
[ -n "$dashboard_start_out" ] && echo "$dashboard_start_out"
|
|
80
|
+
if [ -f "$DASHBOARD_UNAVAILABLE" ]; then
|
|
81
|
+
echo ""
|
|
82
|
+
echo "--- dashboard availability ($DASHBOARD_UNAVAILABLE) ---"
|
|
83
|
+
cat "$DASHBOARD_UNAVAILABLE" 2>/dev/null || true
|
|
84
|
+
fi
|
|
85
|
+
show_log_tail "dashboard" "$DASHBOARD_LOG"
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if [ "$failed" = "1" ]; then
|
|
90
|
+
echo ""
|
|
91
|
+
echo "Not opening the browser because one or more services failed to start."
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
URL="http://localhost:3001"
|
|
96
|
+
PY_BIN=$(claude_smart_resolve_python || true)
|
|
97
|
+
if [ -n "$PY_BIN" ] && "$PY_BIN" -m webbrowser "$URL"; then
|
|
98
|
+
echo "Opened $URL"
|
|
99
|
+
elif command -v open >/dev/null 2>&1 && open "$URL"; then
|
|
100
|
+
echo "Opened $URL"
|
|
101
|
+
elif command -v xdg-open >/dev/null 2>&1 && xdg-open "$URL"; then
|
|
102
|
+
echo "Opened $URL"
|
|
103
|
+
elif command -v powershell >/dev/null 2>&1 && powershell -NoProfile -Command "Start-Process '$URL'"; then
|
|
104
|
+
echo "Opened $URL"
|
|
105
|
+
else
|
|
106
|
+
echo "Dashboard is running at $URL"
|
|
107
|
+
fi
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Auto-start the claude-smart Next.js dashboard (port 3001) if it's not
|
|
3
|
+
# already running. Mirrors how claude-mem boots its worker on SessionStart:
|
|
4
|
+
# detached, returns immediately so the hook doesn't block the session.
|
|
5
|
+
#
|
|
6
|
+
# Subcommands:
|
|
7
|
+
# start probe the port; spawn `npm run start` if our dashboard
|
|
8
|
+
# isn't already answering. Never builds in foreground — if
|
|
9
|
+
# .next is missing, logs and bails (Setup is responsible for
|
|
10
|
+
# the build; rerun it or restart Claude Code to retry).
|
|
11
|
+
# stop kill the recorded process group, and (if our dashboard
|
|
12
|
+
# is still responding on the port) kill the port listener
|
|
13
|
+
# as a fallback — covers dashboards started outside this
|
|
14
|
+
# script or whose PGID signalling missed
|
|
15
|
+
# session-end no-op by default; stops the dashboard if
|
|
16
|
+
# CLAUDE_SMART_DASHBOARD_STOP_ON_END=1 (opt-in — the dashboard
|
|
17
|
+
# is intended to be long-lived across sessions)
|
|
18
|
+
# status print "running on http://localhost:PORT" or "not running"
|
|
19
|
+
set -eu
|
|
20
|
+
|
|
21
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
# shellcheck source=_lib.sh
|
|
23
|
+
. "$HERE/_lib.sh"
|
|
24
|
+
claude_smart_source_login_path
|
|
25
|
+
claude_smart_prepend_node_bins
|
|
26
|
+
|
|
27
|
+
CMD="${1:-start}"
|
|
28
|
+
PORT=3001
|
|
29
|
+
|
|
30
|
+
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
31
|
+
DASHBOARD_DIR="$PLUGIN_ROOT/dashboard"
|
|
32
|
+
WORKSPACE_CWD="${PWD:-}"
|
|
33
|
+
|
|
34
|
+
STATE_DIR="$HOME/.claude-smart"
|
|
35
|
+
PID_FILE="$STATE_DIR/dashboard.pid"
|
|
36
|
+
LOG_FILE="$STATE_DIR/dashboard.log"
|
|
37
|
+
mkdir -p "$STATE_DIR"
|
|
38
|
+
|
|
39
|
+
emit_ok() { echo '{"continue":true,"suppressOutput":true}'; }
|
|
40
|
+
|
|
41
|
+
# Tree-kill the recorded process. Delegates to claude_smart_kill_tree
|
|
42
|
+
# (POSIX: signal the process group; Windows: taskkill /T /F /PID).
|
|
43
|
+
kill_group() {
|
|
44
|
+
claude_smart_kill_tree "$1"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# True if the marker header served by app/api/health is present on the
|
|
48
|
+
# port. Requires curl — absence is reported as false.
|
|
49
|
+
marker_responds() {
|
|
50
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
51
|
+
curl -sfI "http://127.0.0.1:$PORT/api/health" 2>/dev/null \
|
|
52
|
+
| grep -qi '^x-claude-smart-dashboard:'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# True only if *our* dashboard is on the port. Uses the marker header so a
|
|
56
|
+
# foreign listener on 3001 doesn't cause us to silently skip starting.
|
|
57
|
+
is_our_dashboard_running() {
|
|
58
|
+
if [ -f "$PID_FILE" ]; then
|
|
59
|
+
pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
|
|
60
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
61
|
+
# PID alive — still verify the port responds with our marker so we
|
|
62
|
+
# don't claim "running" when the server crashed but the group leader
|
|
63
|
+
# lingered.
|
|
64
|
+
if command -v curl >/dev/null 2>&1; then
|
|
65
|
+
marker_responds && return 0
|
|
66
|
+
else
|
|
67
|
+
# No curl — fall back to PID liveness alone.
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
# No PID or dead PID — probe the port for our marker (recovers after a
|
|
73
|
+
# stale PID file from a crash).
|
|
74
|
+
marker_responds && return 0
|
|
75
|
+
return 1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# True if *something* is listening on the port, regardless of marker.
|
|
79
|
+
port_occupied() {
|
|
80
|
+
if command -v curl >/dev/null 2>&1; then
|
|
81
|
+
curl -sf -o /dev/null "http://127.0.0.1:$PORT" 2>/dev/null && return 0
|
|
82
|
+
# curl with -sfI against a 404/405 still indicates "something answered".
|
|
83
|
+
# Use a connect-only probe as a secondary signal.
|
|
84
|
+
fi
|
|
85
|
+
(echo >"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "$CMD" in
|
|
89
|
+
start)
|
|
90
|
+
# Opt-out: users who don't want the dashboard long-lived can set
|
|
91
|
+
# CLAUDE_SMART_DASHBOARD_AUTOSTART=0 in their environment.
|
|
92
|
+
if [ "${CLAUDE_SMART_DASHBOARD_AUTOSTART:-1}" = "0" ]; then
|
|
93
|
+
emit_ok; exit 0
|
|
94
|
+
fi
|
|
95
|
+
if [ ! -d "$DASHBOARD_DIR" ]; then emit_ok; exit 0; fi
|
|
96
|
+
if is_our_dashboard_running; then claude_smart_clear_dashboard_unavailable; emit_ok; exit 0; fi
|
|
97
|
+
if port_occupied; then
|
|
98
|
+
echo "[claude-smart] dashboard: port $PORT held by another process; skipping" >>"$LOG_FILE"
|
|
99
|
+
emit_ok; exit 0
|
|
100
|
+
fi
|
|
101
|
+
NPM_BIN=$(claude_smart_resolve_npm || true)
|
|
102
|
+
if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
|
|
103
|
+
reason="npm is not on PATH; dashboard cannot start"
|
|
104
|
+
echo "[claude-smart] dashboard: $reason; skipping" >>"$LOG_FILE"
|
|
105
|
+
claude_smart_write_dashboard_unavailable "$reason"
|
|
106
|
+
emit_ok; exit 0
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# `npm run start` requires a prior `next build`. Do NOT build in the
|
|
110
|
+
# foreground here — SessionStart hooks have a tight timeout and a cold
|
|
111
|
+
# Next build easily exceeds it. If .next is missing, spawn a detached
|
|
112
|
+
# build (dashboard-build.sh) so the first-install cost is paid out of
|
|
113
|
+
# band. dashboard-open.sh detects the build-pid file to surface a
|
|
114
|
+
# "still building" message instead of a generic error.
|
|
115
|
+
if [ ! -d "$DASHBOARD_DIR/.next" ]; then
|
|
116
|
+
BUILD_PID_FILE="$STATE_DIR/dashboard-build.pid"
|
|
117
|
+
if ! claude_smart_pid_alive_file "$BUILD_PID_FILE"; then
|
|
118
|
+
echo "[claude-smart] dashboard: .next missing — starting background build (~1-2 min)" >>"$LOG_FILE"
|
|
119
|
+
claude_smart_spawn_detached bash "$HERE/dashboard-build.sh" >>"$LOG_FILE" 2>&1
|
|
120
|
+
fi
|
|
121
|
+
emit_ok; exit 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
cd "$DASHBOARD_DIR"
|
|
125
|
+
|
|
126
|
+
# Detach so the hook returns immediately. claude_smart_spawn_detached
|
|
127
|
+
# picks the strongest primitive available:
|
|
128
|
+
# - Linux: setsid (puts child in its own session/group, pid==pgid).
|
|
129
|
+
# - macOS: python3 os.setsid + execvp (same effect as setsid).
|
|
130
|
+
# - Windows: nohup alone (no process groups; tree-kill via taskkill).
|
|
131
|
+
# Caller-side `>>file 2>&1` redirection is honoured before the child
|
|
132
|
+
# detaches, so per-OS log paths stay identical.
|
|
133
|
+
export CLAUDE_SMART_DASHBOARD_WORKSPACE="$WORKSPACE_CWD"
|
|
134
|
+
claude_smart_spawn_detached "$NPM_BIN" run start >>"$LOG_FILE" 2>&1
|
|
135
|
+
dash_pid=$!
|
|
136
|
+
# Record the spawned pid, not a pgid sampled with ps. On POSIX,
|
|
137
|
+
# setsid/python os.setsid make this pid the new process group leader;
|
|
138
|
+
# sampling immediately can race and capture the caller's pgid instead.
|
|
139
|
+
# On Windows, claude_smart_kill_tree translates the MSYS pid to WINPID.
|
|
140
|
+
echo "$dash_pid" > "$PID_FILE"
|
|
141
|
+
dashboard_ready=0
|
|
142
|
+
for _ in 1 2 3 4 5; do
|
|
143
|
+
if marker_responds; then
|
|
144
|
+
dashboard_ready=1
|
|
145
|
+
claude_smart_clear_dashboard_unavailable
|
|
146
|
+
break
|
|
147
|
+
fi
|
|
148
|
+
sleep 1
|
|
149
|
+
done
|
|
150
|
+
if [ "$dashboard_ready" != "1" ]; then
|
|
151
|
+
claude_smart_write_dashboard_unavailable "dashboard process spawned but did not respond on http://127.0.0.1:$PORT within 5s; see $LOG_FILE"
|
|
152
|
+
fi
|
|
153
|
+
emit_ok
|
|
154
|
+
;;
|
|
155
|
+
stop)
|
|
156
|
+
if [ -f "$PID_FILE" ]; then
|
|
157
|
+
kill_group "$(cat "$PID_FILE" 2>/dev/null)"
|
|
158
|
+
rm -f "$PID_FILE"
|
|
159
|
+
fi
|
|
160
|
+
# Fallback: if our dashboard is still responding on the port (e.g.,
|
|
161
|
+
# was started outside this script, or the PGID kill missed because
|
|
162
|
+
# the process wasn't the group leader) kill whoever owns the port.
|
|
163
|
+
# Gated on the marker header so we never touch a foreign listener.
|
|
164
|
+
if marker_responds && command -v lsof >/dev/null 2>&1; then
|
|
165
|
+
port_pid=$(lsof -t -i ":$PORT" -sTCP:LISTEN 2>/dev/null | head -n1)
|
|
166
|
+
if [ -n "$port_pid" ]; then
|
|
167
|
+
kill -TERM "$port_pid" 2>/dev/null || true
|
|
168
|
+
for _ in 1 2 3 4 5; do
|
|
169
|
+
kill -0 "$port_pid" 2>/dev/null || break
|
|
170
|
+
sleep 0.2
|
|
171
|
+
done
|
|
172
|
+
kill -KILL "$port_pid" 2>/dev/null || true
|
|
173
|
+
fi
|
|
174
|
+
fi
|
|
175
|
+
emit_ok
|
|
176
|
+
;;
|
|
177
|
+
session-end)
|
|
178
|
+
# Default: leave the dashboard running so users can keep browsing
|
|
179
|
+
# interactions/playbooks between sessions. Opt in to teardown by setting
|
|
180
|
+
# CLAUDE_SMART_DASHBOARD_STOP_ON_END=1 in the environment.
|
|
181
|
+
if [ "${CLAUDE_SMART_DASHBOARD_STOP_ON_END:-0}" = "1" ]; then
|
|
182
|
+
if [ -f "$PID_FILE" ]; then
|
|
183
|
+
kill_group "$(cat "$PID_FILE" 2>/dev/null)"
|
|
184
|
+
rm -f "$PID_FILE"
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
emit_ok
|
|
188
|
+
;;
|
|
189
|
+
status)
|
|
190
|
+
if is_our_dashboard_running; then echo "running on http://localhost:$PORT"; else echo "not running"; fi
|
|
191
|
+
;;
|
|
192
|
+
*)
|
|
193
|
+
emit_ok
|
|
194
|
+
;;
|
|
195
|
+
esac
|