claude-smart 0.2.22 → 0.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +69 -27
  3. package/bin/claude-smart.js +296 -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 +1273 -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,37 @@
1
+ {
2
+ "name": "claude-smart-dashboard",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev -p 3001 -H 127.0.0.1",
7
+ "build": "next build",
8
+ "start": "next start -p 3001 -H 127.0.0.1",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@base-ui/react": "^1.3.0",
13
+ "class-variance-authority": "^0.7.1",
14
+ "clsx": "^2.1.1",
15
+ "lucide-react": "^0.577.0",
16
+ "next": "16.2.0",
17
+ "next-themes": "^0.4.6",
18
+ "react": "19.2.4",
19
+ "react-dom": "19.2.4",
20
+ "tailwind-merge": "^3.5.0",
21
+ "tw-animate-css": "^1.4.0"
22
+ },
23
+ "devDependencies": {
24
+ "@tailwindcss/postcss": "^4",
25
+ "@types/node": "^20",
26
+ "@types/react": "^19",
27
+ "@types/react-dom": "^19",
28
+ "eslint": "^9",
29
+ "eslint-config-next": "16.2.0",
30
+ "shadcn": "^4.0.8",
31
+ "tailwindcss": "^4",
32
+ "typescript": "^5"
33
+ },
34
+ "engines": {
35
+ "node": ">=20.9.0"
36
+ }
37
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
@@ -0,0 +1,67 @@
1
+ {
2
+ "description": "claude-smart hooks for Codex — learn from sessions via reflexio",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "matcher": "startup|resume",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/ensure-plugin-root.sh\" \"$_R\" || true",
11
+ "timeout": 10
12
+ },
13
+ {
14
+ "type": "command",
15
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/backend-service.sh\" start",
16
+ "timeout": 30
17
+ },
18
+ {
19
+ "type": "command",
20
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/dashboard-service.sh\" start",
21
+ "timeout": 10
22
+ },
23
+ {
24
+ "type": "command",
25
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex session-start",
26
+ "timeout": 30,
27
+ "statusMessage": "Loading claude-smart context"
28
+ }
29
+ ]
30
+ }
31
+ ],
32
+ "UserPromptSubmit": [
33
+ {
34
+ "hooks": [
35
+ {
36
+ "type": "command",
37
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex user-prompt",
38
+ "timeout": 15
39
+ }
40
+ ]
41
+ }
42
+ ],
43
+ "PostToolUse": [
44
+ {
45
+ "matcher": ".*",
46
+ "hooks": [
47
+ {
48
+ "type": "command",
49
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex post-tool",
50
+ "timeout": 15
51
+ }
52
+ ]
53
+ }
54
+ ],
55
+ "Stop": [
56
+ {
57
+ "hooks": [
58
+ {
59
+ "type": "command",
60
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex stop",
61
+ "timeout": 30
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ }
67
+ }
@@ -0,0 +1,111 @@
1
+ {
2
+ "description": "claude-smart hooks — learn from Claude Code sessions via reflexio",
3
+ "hooks": {
4
+ "Setup": [
5
+ {
6
+ "matcher": "*",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/smart-install.sh\"",
11
+ "timeout": 300
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "SessionStart": [
17
+ {
18
+ "matcher": "startup|clear|compact|resume",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/ensure-plugin-root.sh\" \"$_R\" || true",
23
+ "timeout": 10
24
+ },
25
+ {
26
+ "type": "command",
27
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/hook_entry.sh\" claude-code session-start",
28
+ "timeout": 30
29
+ },
30
+ {
31
+ "type": "command",
32
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/backend-service.sh\" start",
33
+ "timeout": 30
34
+ },
35
+ {
36
+ "type": "command",
37
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/dashboard-service.sh\" start",
38
+ "timeout": 10
39
+ }
40
+ ]
41
+ }
42
+ ],
43
+ "UserPromptSubmit": [
44
+ {
45
+ "hooks": [
46
+ {
47
+ "type": "command",
48
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/hook_entry.sh\" claude-code user-prompt",
49
+ "timeout": 15
50
+ }
51
+ ]
52
+ }
53
+ ],
54
+ "PreToolUse": [
55
+ {
56
+ "matcher": "Edit|Write|NotebookEdit|Bash",
57
+ "hooks": [
58
+ {
59
+ "type": "command",
60
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/hook_entry.sh\" claude-code pre-tool",
61
+ "timeout": 10
62
+ }
63
+ ]
64
+ }
65
+ ],
66
+ "PostToolUse": [
67
+ {
68
+ "matcher": "*",
69
+ "hooks": [
70
+ {
71
+ "type": "command",
72
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/hook_entry.sh\" claude-code post-tool",
73
+ "timeout": 15
74
+ }
75
+ ]
76
+ }
77
+ ],
78
+ "Stop": [
79
+ {
80
+ "hooks": [
81
+ {
82
+ "type": "command",
83
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/hook_entry.sh\" claude-code stop",
84
+ "timeout": 30
85
+ }
86
+ ]
87
+ }
88
+ ],
89
+ "SessionEnd": [
90
+ {
91
+ "hooks": [
92
+ {
93
+ "type": "command",
94
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/hook_entry.sh\" claude-code session-end",
95
+ "timeout": 60
96
+ },
97
+ {
98
+ "type": "command",
99
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/dashboard-service.sh\" session-end",
100
+ "timeout": 10
101
+ },
102
+ {
103
+ "type": "command",
104
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/backend-service.sh\" session-end",
105
+ "timeout": 10
106
+ }
107
+ ]
108
+ }
109
+ ]
110
+ }
111
+ }
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "claude-smart"
3
+ version = "0.2.24"
4
+ description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "reflexio-ai>=0.2.22",
9
+ # Used by reflexio's local embedding provider (ONNXMiniLM_L6_V2).
10
+ # Pulls in onnxruntime + tokenizers; the ~80 MB ONNX model itself is
11
+ # downloaded on first use, not at install time.
12
+ "chromadb>=0.5",
13
+ ]
14
+
15
+ [project.scripts]
16
+ claude-smart-hook = "claude_smart.hook:main"
17
+ claude-smart = "claude_smart.cli:main"
18
+ claude-smart-optimizer-assistant = "claude_smart.optimizer_assistant:main"
19
+
20
+ # NOTE: LOCAL DEV ONLY — do not commit this block to main.
21
+ # Published installs must resolve reflexio-ai from PyPI. This override
22
+ # points at the vendored submodule so editable changes flow through.
23
+ # `git update-index --skip-worktree plugin/pyproject.toml plugin/uv.lock`
24
+ # hides the local divergence from `git status`.
25
+ # [tool.uv.sources]
26
+ # reflexio-ai = { path = "/Users/yilu/repos/claude-smart/reflexio", editable = true }
27
+
28
+ [build-system]
29
+ requires = ["hatchling"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/claude_smart"]
34
+
35
+ [tool.hatch.build.targets.sdist]
36
+ include = [
37
+ "src/claude_smart",
38
+ "pyproject.toml",
39
+ "README.md",
40
+ "LICENSE",
41
+ ]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["../tests"]
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "pytest>=9.0.3",
49
+ ]
@@ -0,0 +1,27 @@
1
+ # Shared bootstrap for Codex hook commands. This file is sourced by hook
2
+ # entries after a tiny root-discovery step, so keep it POSIX-shell compatible.
3
+
4
+ _HP=$(printenv PATH 2>/dev/null || true)
5
+ if [ -z "$_HP" ] && [ -n "${SHELL:-}" ]; then
6
+ _HP=$("$SHELL" -lc 'printf %s "$PATH"' 2>/dev/null || true)
7
+ fi
8
+ _HP=$(printf '%s' "$_HP" | tr ' ' ':')
9
+ export PATH="${_HP:+$_HP:}$PATH"
10
+
11
+ if [ -z "${_R:-}" ]; then
12
+ _R="${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}"
13
+ fi
14
+ if [ -z "$_R" ]; then
15
+ _R=$(ls -dt "$HOME/plugins/claude-smart" \
16
+ "$HOME/.codex/plugins/cache/reflexioai/claude-smart"/*/ \
17
+ 2>/dev/null | head -n 1)
18
+ fi
19
+ if [ -z "$_R" ]; then
20
+ echo "claude-smart: plugin root not found" >&2
21
+ return 1 2>/dev/null || exit 1
22
+ fi
23
+
24
+ _R="${_R%/}"
25
+ export _R
26
+ export PLUGIN_ROOT="$_R"
27
+ export CLAUDE_PLUGIN_ROOT="$_R"
@@ -0,0 +1,325 @@
1
+ # Shared helpers for claude-smart plugin scripts. Source, do not execute.
2
+
3
+ # Claude Code hooks run with a minimal non-interactive PATH that often omits
4
+ # nvm/asdf/brew shims where `npm`, `uv`, etc. live. Pull the user's login-shell
5
+ # PATH the same way claude-mem does so hook-spawned scripts find them without
6
+ # the user having to mutate their global PATH. Best-effort — failures silent.
7
+ claude_smart_source_login_path() {
8
+ local _SHELL_PATH
9
+ if [ -n "${SHELL:-}" ] && [ -x "$SHELL" ]; then
10
+ if _SHELL_PATH="$("$SHELL" -lc 'printf %s "$PATH"' 2>/dev/null)"; then
11
+ [ -n "$_SHELL_PATH" ] && export PATH="$_SHELL_PATH:$PATH"
12
+ fi
13
+ fi
14
+ }
15
+
16
+ # Prepend the astral.sh installer's default bin directories to PATH so a
17
+ # freshly-installed `uv` is reachable before the user re-sources their
18
+ # shell rc. Prepend (not append) so the just-installed binary wins over
19
+ # any stale copy earlier in PATH. Literals only — no subshell, so safe
20
+ # under `set -u`.
21
+ claude_smart_prepend_astral_bins() {
22
+ export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH"
23
+ }
24
+
25
+ # Prepend claude-smart's private Node.js install, if present. The Setup
26
+ # hook installs Node here when the user does not already have a suitable
27
+ # node/npm on PATH, so later hook-spawned dashboard scripts can run without
28
+ # requiring nvm/brew/global npm to be visible from Claude Code's shell.
29
+ claude_smart_prepend_node_bins() {
30
+ local _CS_NODE_ROOT
31
+ _CS_NODE_ROOT="$HOME/.claude-smart/node/current"
32
+ export PATH="$_CS_NODE_ROOT/bin:$_CS_NODE_ROOT:$PATH"
33
+ }
34
+
35
+ claude_smart_dashboard_unavailable_marker() {
36
+ printf '%s\n' "$HOME/.claude-smart/dashboard-unavailable"
37
+ }
38
+
39
+ claude_smart_node_recovery_hint() {
40
+ cat <<'EOF'
41
+ Recovery:
42
+ 1. Restart Claude Code to let claude-smart retry its private Node.js install.
43
+ 2. If the retry is blocked by your network or OS policy, install Node.js 20.9+ manually:
44
+ EOF
45
+ if claude_smart_is_windows; then
46
+ cat <<'EOF'
47
+ - winget install OpenJS.NodeJS.LTS
48
+ - or download the LTS installer from https://nodejs.org/
49
+ EOF
50
+ elif [ "$(uname -s 2>/dev/null)" = "Darwin" ]; then
51
+ cat <<'EOF'
52
+ - brew install node
53
+ - or download the LTS installer from https://nodejs.org/
54
+ EOF
55
+ else
56
+ cat <<'EOF'
57
+ - use your distro package manager for nodejs/npm
58
+ - or download the LTS archive from https://nodejs.org/
59
+ EOF
60
+ fi
61
+ cat <<'EOF'
62
+ 3. Run /claude-smart:restart, then /claude-smart:dashboard.
63
+ EOF
64
+ }
65
+
66
+ claude_smart_write_dashboard_unavailable() {
67
+ local reason marker
68
+ reason="$1"
69
+ marker="$(claude_smart_dashboard_unavailable_marker)"
70
+ mkdir -p "$(dirname "$marker")"
71
+ {
72
+ printf 'claude-smart dashboard is unavailable: %s\n\n' "$reason"
73
+ printf 'The learning backend and hooks can still work; only the dashboard UI is affected.\n\n'
74
+ printf 'Private Node.js location: %s\n\n' "$HOME/.claude-smart/node/current"
75
+ claude_smart_node_recovery_hint
76
+ } > "$marker"
77
+ }
78
+
79
+ claude_smart_clear_dashboard_unavailable() {
80
+ rm -f "$(claude_smart_dashboard_unavailable_marker)"
81
+ }
82
+
83
+ # Return 0 (true) if running under a Windows-flavoured bash (Git Bash,
84
+ # MSYS, Cygwin). Used to gate POSIX-only primitives (setsid, process
85
+ # groups) and route around Windows-specific potholes (the python3 App
86
+ # Execution Alias stub at WindowsApps\python3.exe).
87
+ claude_smart_is_windows() {
88
+ case "$(uname -s 2>/dev/null)" in
89
+ MINGW*|MSYS*|CYGWIN*) return 0 ;;
90
+ *) return 1 ;;
91
+ esac
92
+ }
93
+
94
+ # Print the absolute path of a working python interpreter, or nothing
95
+ # (and return non-zero) if none is usable. On Windows, `python3` is
96
+ # usually the Microsoft Store "App Execution Alias" stub at
97
+ # %LocalAppData%\Microsoft\WindowsApps\python3.exe — `command -v python3`
98
+ # returns truthy but invoking it just prints a "Python was not found"
99
+ # message and exits non-zero. We probe with `-V` to filter the stub out
100
+ # and prefer `python` (the real interpreter when one is installed).
101
+ claude_smart_resolve_python() {
102
+ if claude_smart_is_windows; then
103
+ for cand in python python3; do
104
+ if command -v "$cand" >/dev/null 2>&1 && "$cand" -V >/dev/null 2>&1; then
105
+ command -v "$cand"
106
+ return 0
107
+ fi
108
+ done
109
+ return 1
110
+ fi
111
+ for cand in python3 python; do
112
+ if command -v "$cand" >/dev/null 2>&1 && "$cand" -V >/dev/null 2>&1; then
113
+ command -v "$cand"
114
+ return 0
115
+ fi
116
+ done
117
+ return 1
118
+ }
119
+
120
+ claude_smart_download() {
121
+ local url dest src _CS_PY
122
+ url="$1"
123
+ dest="$2"
124
+ mkdir -p "$(dirname "$dest")"
125
+ case "$url" in
126
+ file://*)
127
+ src="${url#file://}"
128
+ cp "$src" "$dest"
129
+ return $?
130
+ ;;
131
+ esac
132
+ if command -v curl >/dev/null 2>&1; then
133
+ curl -fsSL "$url" -o "$dest"
134
+ return $?
135
+ fi
136
+ if command -v wget >/dev/null 2>&1; then
137
+ wget -q -O "$dest" "$url"
138
+ return $?
139
+ fi
140
+ if command -v powershell >/dev/null 2>&1; then
141
+ URL="$url" DEST="$dest" powershell -NoProfile -ExecutionPolicy Bypass -Command \
142
+ '$ProgressPreference="SilentlyContinue"; Invoke-WebRequest -Uri $env:URL -OutFile $env:DEST'
143
+ return $?
144
+ fi
145
+ if command -v pwsh >/dev/null 2>&1; then
146
+ URL="$url" DEST="$dest" pwsh -NoProfile -Command \
147
+ '$ProgressPreference="SilentlyContinue"; Invoke-WebRequest -Uri $env:URL -OutFile $env:DEST'
148
+ return $?
149
+ fi
150
+ _CS_PY=$(claude_smart_resolve_python || true)
151
+ if [ -n "$_CS_PY" ]; then
152
+ "$_CS_PY" - "$url" "$dest" <<'PY'
153
+ import sys
154
+ import urllib.request
155
+
156
+ url, dest = sys.argv[1], sys.argv[2]
157
+ with urllib.request.urlopen(url, timeout=60) as response:
158
+ data = response.read()
159
+ with open(dest, "wb") as fh:
160
+ fh.write(data)
161
+ PY
162
+ return $?
163
+ fi
164
+ return 1
165
+ }
166
+
167
+ claude_smart_sha256_file() {
168
+ local path _CS_PY
169
+ path="$1"
170
+ if command -v sha256sum >/dev/null 2>&1; then
171
+ sha256sum "$path" | awk '{print $1}'
172
+ return ${PIPESTATUS[0]}
173
+ fi
174
+ if command -v shasum >/dev/null 2>&1; then
175
+ shasum -a 256 "$path" | awk '{print $1}'
176
+ return ${PIPESTATUS[0]}
177
+ fi
178
+ if command -v openssl >/dev/null 2>&1; then
179
+ openssl dgst -sha256 "$path" | awk '{print $NF}'
180
+ return ${PIPESTATUS[0]}
181
+ fi
182
+ _CS_PY=$(claude_smart_resolve_python || true)
183
+ if [ -n "$_CS_PY" ]; then
184
+ "$_CS_PY" - "$path" <<'PY'
185
+ import hashlib
186
+ import sys
187
+
188
+ h = hashlib.sha256()
189
+ with open(sys.argv[1], "rb") as fh:
190
+ for chunk in iter(lambda: fh.read(1024 * 1024), b""):
191
+ h.update(chunk)
192
+ print(h.hexdigest())
193
+ PY
194
+ return $?
195
+ fi
196
+ return 1
197
+ }
198
+
199
+ # Return 0 if `node` exists and satisfies the minimum major/minor pair.
200
+ # Patch versions are intentionally ignored because our requirement is a
201
+ # floor, not an exact runtime pin.
202
+ claude_smart_node_satisfies() {
203
+ local min_major min_minor version major minor
204
+ min_major="$1"
205
+ min_minor="$2"
206
+ command -v node >/dev/null 2>&1 || return 1
207
+ version=$(node -v 2>/dev/null | sed 's/^v//') || return 1
208
+ major=$(printf '%s' "$version" | awk -F. '{print $1}')
209
+ minor=$(printf '%s' "$version" | awk -F. '{print $2}')
210
+ [ -n "$major" ] && [ -n "$minor" ] || return 1
211
+ case "$major:$minor" in
212
+ *[!0-9:]*|:*|*:) return 1 ;;
213
+ esac
214
+ [ "$major" -gt "$min_major" ] && return 0
215
+ [ "$major" -eq "$min_major" ] && [ "$minor" -ge "$min_minor" ]
216
+ }
217
+
218
+ claude_smart_resolve_npm() {
219
+ local cand
220
+ for cand in npm npm.cmd; do
221
+ if command -v "$cand" >/dev/null 2>&1; then
222
+ command -v "$cand"
223
+ return 0
224
+ fi
225
+ done
226
+ return 1
227
+ }
228
+
229
+ claude_smart_npm_available() {
230
+ local npm_bin
231
+ npm_bin=$(claude_smart_resolve_npm || true)
232
+ [ -n "$npm_bin" ] || return 1
233
+ "$npm_bin" --version >/dev/null 2>&1
234
+ }
235
+
236
+ # Spawn a command fully detached from the current shell so a hook timeout
237
+ # (Claude Code's install/SessionStart budget) cannot kill it mid-flight.
238
+ # POSIX: setsid → python3 os.setsid → nohup (in that order of strength).
239
+ # Windows: nohup alone — Git Bash has no setsid, no process groups, and
240
+ # `os.setsid()` is POSIX-only; nohup ignores SIGHUP which is enough to
241
+ # survive the parent console closing. The python3 fallback is gated on a
242
+ # real-interpreter probe (-V) so the Windows App Execution Alias stub
243
+ # doesn't get invoked. Caller is responsible for redirecting stdout/stderr;
244
+ # we do not impose a log destination here. Stdin is closed so the child
245
+ # cannot inherit a tty. Use `$!` after this call to capture the pid.
246
+ claude_smart_spawn_detached() {
247
+ if claude_smart_is_windows; then
248
+ nohup "$@" < /dev/null &
249
+ return 0
250
+ fi
251
+ if command -v setsid >/dev/null 2>&1; then
252
+ setsid nohup "$@" < /dev/null &
253
+ elif _CS_PY=$(claude_smart_resolve_python) && [ -n "$_CS_PY" ]; then
254
+ "$_CS_PY" -c 'import os,sys; os.setsid(); os.execvp(sys.argv[1], sys.argv[1:])' \
255
+ "$@" < /dev/null &
256
+ else
257
+ nohup "$@" < /dev/null &
258
+ fi
259
+ }
260
+
261
+ # Terminate a process and (on POSIX) its whole process group, escalating
262
+ # from TERM to KILL after a short grace period. On Windows there are no
263
+ # POSIX process groups, so we use `taskkill /T /F /PID` which walks the
264
+ # child-process tree via the Windows job-object/parent-pid relationships
265
+ # — the closest equivalent to a group kill.
266
+ #
267
+ # Windows-specific subtlety: in Git Bash / MSYS, `$!` for a backgrounded
268
+ # job returns the MSYS pid (an internal counter), NOT the native Windows
269
+ # pid that taskkill needs. `ps -W` (or `-o winpid=`) exposes the WINPID
270
+ # column for the translation. If the lookup fails we fall back to
271
+ # treating the input as a native pid, so callers can pass either an MSYS
272
+ # pid (recorded via $!) or a Windows pid (from tasklist) interchangeably.
273
+ # The `//T //F //PID` syntax escapes Git Bash's MSYS path-mangling of
274
+ # arguments that begin with `/`.
275
+ claude_smart_kill_tree() {
276
+ pid="$1"
277
+ [ -z "$pid" ] && return 0
278
+ if claude_smart_is_windows; then
279
+ # Git Bash's `ps` is the procps fork, not BSD/Linux ps; it has no
280
+ # -o option but its default header is `PID PPID PGID WINPID TTY ...`,
281
+ # so column 4 of the data row is the Windows pid. awk extracts it
282
+ # without depending on -o support.
283
+ target=""
284
+ if command -v ps >/dev/null 2>&1; then
285
+ target=$(ps -p "$pid" 2>/dev/null | awk 'NR==2 {print $4}' | tr -d ' \r\n' || true)
286
+ fi
287
+ [ -z "$target" ] && target="$pid"
288
+ if command -v taskkill >/dev/null 2>&1; then
289
+ taskkill //T //F //PID "$target" >/dev/null 2>&1 || true
290
+ else
291
+ kill -TERM "$pid" 2>/dev/null || true
292
+ sleep 0.5
293
+ kill -KILL "$pid" 2>/dev/null || true
294
+ fi
295
+ return 0
296
+ fi
297
+ current_pgid=""
298
+ if command -v ps >/dev/null 2>&1; then
299
+ current_pgid=$(ps -o pgid= -p "$$" 2>/dev/null | tr -d ' ')
300
+ fi
301
+ if [ -n "$current_pgid" ] && [ "$pid" = "$current_pgid" ]; then
302
+ return 0
303
+ fi
304
+ if ! kill -TERM -- "-$pid" 2>/dev/null; then
305
+ kill -TERM "$pid" 2>/dev/null || true
306
+ sleep 0.5
307
+ kill -KILL "$pid" 2>/dev/null || true
308
+ return 0
309
+ fi
310
+ for _ in 1 2 3 4 5; do
311
+ kill -0 -- "-$pid" 2>/dev/null || return 0
312
+ sleep 0.2
313
+ done
314
+ kill -KILL -- "-$pid" 2>/dev/null || true
315
+ }
316
+
317
+ # Return 0 (true) if $1 names a pid file whose pid is currently alive.
318
+ # Silent on missing/empty/stale files.
319
+ claude_smart_pid_alive_file() {
320
+ pid_file="$1"
321
+ [ -f "$pid_file" ] || return 1
322
+ pid=$(cat "$pid_file" 2>/dev/null || echo "")
323
+ [ -n "$pid" ] || return 1
324
+ kill -0 "$pid" 2>/dev/null
325
+ }