claude-smart 0.2.23 → 0.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +76 -28
  3. package/bin/claude-smart.js +355 -11
  4. package/package.json +11 -1
  5. package/plugin/.claude-plugin/plugin.json +17 -0
  6. package/plugin/.codex-plugin/plugin.json +35 -0
  7. package/plugin/LICENSE +202 -0
  8. package/plugin/README.md +37 -0
  9. package/plugin/bin/cs-cite +77 -0
  10. package/plugin/commands/clear-all.md +8 -0
  11. package/plugin/commands/dashboard.md +8 -0
  12. package/plugin/commands/learn.md +12 -0
  13. package/plugin/commands/restart.md +8 -0
  14. package/plugin/commands/show.md +8 -0
  15. package/plugin/dashboard/AGENTS.md +6 -0
  16. package/plugin/dashboard/app/api/claude-settings/route.ts +19 -0
  17. package/plugin/dashboard/app/api/config/route.ts +16 -0
  18. package/plugin/dashboard/app/api/health/route.ts +10 -0
  19. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +63 -0
  20. package/plugin/dashboard/app/api/sessions/[id]/route.ts +28 -0
  21. package/plugin/dashboard/app/api/sessions/route.ts +14 -0
  22. package/plugin/dashboard/app/configure/env/page.tsx +318 -0
  23. package/plugin/dashboard/app/configure/layout.tsx +47 -0
  24. package/plugin/dashboard/app/configure/page.tsx +5 -0
  25. package/plugin/dashboard/app/configure/server/page.tsx +258 -0
  26. package/plugin/dashboard/app/dashboard/page.tsx +227 -0
  27. package/plugin/dashboard/app/globals.css +129 -0
  28. package/plugin/dashboard/app/icon.png +0 -0
  29. package/plugin/dashboard/app/layout.tsx +40 -0
  30. package/plugin/dashboard/app/page.tsx +5 -0
  31. package/plugin/dashboard/app/preferences/[id]/page.tsx +531 -0
  32. package/plugin/dashboard/app/preferences/page.tsx +126 -0
  33. package/plugin/dashboard/app/providers.tsx +12 -0
  34. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +321 -0
  35. package/plugin/dashboard/app/sessions/page.tsx +186 -0
  36. package/plugin/dashboard/app/skills/page.tsx +362 -0
  37. package/plugin/dashboard/app/skills/project/[id]/page.tsx +597 -0
  38. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +830 -0
  39. package/plugin/dashboard/components/common/delete-all-button.tsx +45 -0
  40. package/plugin/dashboard/components/common/empty-state.tsx +34 -0
  41. package/plugin/dashboard/components/common/learnings-badge.tsx +34 -0
  42. package/plugin/dashboard/components/common/page-header.tsx +34 -0
  43. package/plugin/dashboard/components/common/page-tabs.tsx +115 -0
  44. package/plugin/dashboard/components/common/stat-card.tsx +38 -0
  45. package/plugin/dashboard/components/layout/nav-items.ts +22 -0
  46. package/plugin/dashboard/components/layout/sidebar.tsx +45 -0
  47. package/plugin/dashboard/components/layout/top-bar.tsx +64 -0
  48. package/plugin/dashboard/components/stall-banner.tsx +53 -0
  49. package/plugin/dashboard/components/ui/badge.tsx +52 -0
  50. package/plugin/dashboard/components/ui/button.tsx +60 -0
  51. package/plugin/dashboard/components/ui/collapsible.tsx +21 -0
  52. package/plugin/dashboard/components/ui/input.tsx +20 -0
  53. package/plugin/dashboard/components/ui/label.tsx +20 -0
  54. package/plugin/dashboard/components/ui/scroll-area.tsx +55 -0
  55. package/plugin/dashboard/components/ui/select.tsx +201 -0
  56. package/plugin/dashboard/components/ui/separator.tsx +25 -0
  57. package/plugin/dashboard/components/ui/sheet.tsx +135 -0
  58. package/plugin/dashboard/components/ui/switch.tsx +32 -0
  59. package/plugin/dashboard/components.json +25 -0
  60. package/plugin/dashboard/eslint.config.mjs +16 -0
  61. package/plugin/dashboard/hooks/use-settings.tsx +88 -0
  62. package/plugin/dashboard/hooks/use-stall-state.ts +59 -0
  63. package/plugin/dashboard/lib/claude-settings-file.ts +114 -0
  64. package/plugin/dashboard/lib/config-file.ts +131 -0
  65. package/plugin/dashboard/lib/format.ts +58 -0
  66. package/plugin/dashboard/lib/reflexio-client.ts +238 -0
  67. package/plugin/dashboard/lib/reflexio-url.ts +17 -0
  68. package/plugin/dashboard/lib/session-reader.ts +245 -0
  69. package/plugin/dashboard/lib/status.ts +24 -0
  70. package/plugin/dashboard/lib/types.ts +145 -0
  71. package/plugin/dashboard/lib/utils.ts +6 -0
  72. package/plugin/dashboard/next.config.ts +7 -0
  73. package/plugin/dashboard/package-lock.json +10275 -0
  74. package/plugin/dashboard/package.json +37 -0
  75. package/plugin/dashboard/postcss.config.mjs +7 -0
  76. package/plugin/dashboard/public/claude-smart-icon.png +0 -0
  77. package/plugin/dashboard/tsconfig.json +34 -0
  78. package/plugin/hooks/codex-hooks.json +67 -0
  79. package/plugin/hooks/hooks.json +111 -0
  80. package/plugin/pyproject.toml +49 -0
  81. package/plugin/scripts/_codex_env.sh +27 -0
  82. package/plugin/scripts/_lib.sh +325 -0
  83. package/plugin/scripts/backend-service.sh +208 -0
  84. package/plugin/scripts/cli.sh +40 -0
  85. package/plugin/scripts/dashboard-build.sh +139 -0
  86. package/plugin/scripts/dashboard-open.sh +107 -0
  87. package/plugin/scripts/dashboard-service.sh +195 -0
  88. package/plugin/scripts/ensure-plugin-root.sh +84 -0
  89. package/plugin/scripts/hook_entry.sh +70 -0
  90. package/plugin/scripts/smart-install.sh +411 -0
  91. package/plugin/src/claude_smart/__init__.py +3 -0
  92. package/plugin/src/claude_smart/cli.py +1342 -0
  93. package/plugin/src/claude_smart/context_format.py +277 -0
  94. package/plugin/src/claude_smart/context_inject.py +92 -0
  95. package/plugin/src/claude_smart/cs_cite.py +236 -0
  96. package/plugin/src/claude_smart/events/__init__.py +1 -0
  97. package/plugin/src/claude_smart/events/post_tool.py +148 -0
  98. package/plugin/src/claude_smart/events/pre_tool.py +52 -0
  99. package/plugin/src/claude_smart/events/session_end.py +20 -0
  100. package/plugin/src/claude_smart/events/session_start.py +119 -0
  101. package/plugin/src/claude_smart/events/stop.py +393 -0
  102. package/plugin/src/claude_smart/events/user_prompt.py +73 -0
  103. package/plugin/src/claude_smart/hook.py +114 -0
  104. package/plugin/src/claude_smart/ids.py +56 -0
  105. package/plugin/src/claude_smart/internal_call.py +89 -0
  106. package/plugin/src/claude_smart/optimizer_assistant.py +203 -0
  107. package/plugin/src/claude_smart/publish.py +71 -0
  108. package/plugin/src/claude_smart/query_compose.py +51 -0
  109. package/plugin/src/claude_smart/reflexio_adapter.py +403 -0
  110. package/plugin/src/claude_smart/runtime.py +52 -0
  111. package/plugin/src/claude_smart/stall_banner.py +61 -0
  112. package/plugin/src/claude_smart/state.py +276 -0
  113. package/plugin/uv.lock +3720 -0
@@ -0,0 +1,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