claude-smart 0.2.25 → 0.2.27

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.
@@ -7,22 +7,22 @@
7
7
  "hooks": [
8
8
  {
9
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",
10
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$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
11
  "timeout": 10
12
12
  },
13
13
  {
14
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",
15
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$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
16
  "timeout": 30
17
17
  },
18
18
  {
19
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",
20
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$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
21
  "timeout": 10
22
22
  },
23
23
  {
24
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",
25
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$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
26
  "timeout": 30,
27
27
  "statusMessage": "Loading claude-smart context"
28
28
  }
@@ -34,7 +34,7 @@
34
34
  "hooks": [
35
35
  {
36
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",
37
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$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
38
  "timeout": 15
39
39
  }
40
40
  ]
@@ -46,7 +46,7 @@
46
46
  "hooks": [
47
47
  {
48
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",
49
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$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
50
  "timeout": 15
51
51
  }
52
52
  ]
@@ -57,7 +57,7 @@
57
57
  "hooks": [
58
58
  {
59
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",
60
+ "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$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
61
  "timeout": 30
62
62
  }
63
63
  ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.25"
3
+ version = "0.2.27"
4
4
  description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  # Pulls in onnxruntime + tokenizers; the ~80 MB ONNX model itself is
11
11
  # downloaded on first use, not at install time.
12
12
  "chromadb>=0.5",
13
+ "einops>=0.8.0",
13
14
  ]
14
15
 
15
16
  [project.scripts]
@@ -25,3 +25,4 @@ _R="${_R%/}"
25
25
  export _R
26
26
  export PLUGIN_ROOT="$_R"
27
27
  export CLAUDE_PLUGIN_ROOT="$_R"
28
+ export CLAUDE_SMART_HOST="codex"
@@ -29,25 +29,30 @@ PORT=8071
29
29
  # binds to PORT instead of reflexio's library default (8081).
30
30
  export BACKEND_PORT="$PORT"
31
31
 
32
- # Default: route extraction through the local claude CLI + ONNX embedder
32
+ # Default: route extraction through the active host CLI + ONNX embedder
33
33
  # so claude-smart works without any LLM API key. Users can opt out by
34
34
  # pre-exporting these to 0.
35
35
  export CLAUDE_SMART_USE_LOCAL_CLI="${CLAUDE_SMART_USE_LOCAL_CLI:-1}"
36
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
37
+ # The backend can be spawned from contexts whose PATH lacks the host
38
38
  # CLI dir (commonly ~/.local/bin or /opt/homebrew/bin). Pin the CLI
39
39
  # explicitly if we can resolve it from our own (post-login-path) PATH.
40
+ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
41
+
40
42
  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
+ if [ "${CLAUDE_SMART_HOST:-claude-code}" = "codex" ]; then
44
+ # Reflexio's provider still calls CLAUDE_SMART_CLI_PATH with Claude CLI
45
+ # flags. Use a small compatibility executable that translates that narrow
46
+ # contract to `codex exec`.
47
+ export CLAUDE_SMART_CLI_PATH="$PLUGIN_ROOT/scripts/codex-claude-compat.py"
48
+ elif _cs_cli_path=$(command -v claude 2>/dev/null) && [ -n "$_cs_cli_path" ]; then
49
+ export CLAUDE_SMART_CLI_PATH="$_cs_cli_path"
43
50
  elif [ -x "$HOME/.local/bin/claude" ]; then
44
51
  export CLAUDE_SMART_CLI_PATH="$HOME/.local/bin/claude"
45
52
  fi
46
- unset _cs_claude_path
53
+ unset _cs_cli_path
47
54
  fi
48
55
 
49
- PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
50
-
51
56
  STATE_DIR="$HOME/.claude-smart"
52
57
  PID_FILE="$STATE_DIR/backend.pid"
53
58
  LOG_FILE="$STATE_DIR/backend.log"
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """Translate Reflexio's Claude CLI provider contract to ``codex exec``.
3
+
4
+ Reflexio's local provider currently shells out to ``CLAUDE_SMART_CLI_PATH`` as
5
+ if it were the Claude Code CLI:
6
+
7
+ <path> -p --output-format json --model <model> [--append-system-prompt ...]
8
+
9
+ When claude-smart runs under Codex, this executable preserves that narrow
10
+ contract while routing the actual model call through ``codex exec``. The
11
+ stdout shape intentionally matches Claude Code's JSON output enough for
12
+ Reflexio's provider to read ``result``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ from pathlib import Path
24
+
25
+
26
+ _TIMEOUT_SECONDS = 120
27
+
28
+
29
+ def main(argv: list[str] | None = None) -> int:
30
+ argv = list(sys.argv[1:] if argv is None else argv)
31
+ try:
32
+ output_format, system_prompt = _parse_supported_args(argv)
33
+ content = _run_codex(
34
+ prompt=sys.stdin.read(),
35
+ system_prompt=system_prompt,
36
+ )
37
+ except Exception as exc: # noqa: BLE001 - CLI bridge errors go to stderr.
38
+ print(f"codex-claude-compat: {exc}", file=sys.stderr)
39
+ return 1
40
+
41
+ payload = {"result": content}
42
+ if output_format == "stream-json":
43
+ payload = {"type": "result", "subtype": "success", "result": content}
44
+ json.dump(payload, sys.stdout, ensure_ascii=False)
45
+ sys.stdout.write("\n")
46
+ return 0
47
+
48
+
49
+ def _parse_supported_args(argv: list[str]) -> tuple[str, str]:
50
+ output_format = "json"
51
+ system_prompt = ""
52
+ idx = 0
53
+ while idx < len(argv):
54
+ arg = argv[idx]
55
+ if arg == "-p":
56
+ idx += 1
57
+ elif arg == "--output-format":
58
+ if idx + 1 >= len(argv):
59
+ raise ValueError("--output-format requires a value")
60
+ output_format = argv[idx + 1]
61
+ idx += 2
62
+ elif arg == "--model":
63
+ idx += 2
64
+ elif arg in {"--verbose", "--include-partial-messages"}:
65
+ idx += 1
66
+ elif arg == "--append-system-prompt":
67
+ if idx + 1 >= len(argv):
68
+ raise ValueError("--append-system-prompt requires a value")
69
+ system_prompt = argv[idx + 1]
70
+ idx += 2
71
+ else:
72
+ raise ValueError(f"unsupported Claude CLI argument: {arg}")
73
+ if output_format not in {"json", "stream-json"}:
74
+ raise ValueError(f"unsupported --output-format: {output_format}")
75
+ return output_format, system_prompt
76
+
77
+
78
+ def _run_codex(*, prompt: str, system_prompt: str) -> str:
79
+ codex_path = os.environ.get("CLAUDE_SMART_CODEX_PATH") or shutil.which("codex")
80
+ if not codex_path:
81
+ raise FileNotFoundError("codex CLI not found on PATH")
82
+
83
+ output_path = _temporary_output_path()
84
+ cmd = [
85
+ codex_path,
86
+ "exec",
87
+ "--sandbox",
88
+ "read-only",
89
+ "--skip-git-repo-check",
90
+ "--ephemeral",
91
+ "--ignore-rules",
92
+ "--output-last-message",
93
+ str(output_path),
94
+ "-",
95
+ ]
96
+
97
+ env = os.environ.copy()
98
+ env["CLAUDE_SMART_HOST"] = "codex"
99
+ env["CLAUDE_SMART_INTERNAL"] = "1"
100
+ env["CLAUDE_CODE_ENTRYPOINT"] = "optimizer"
101
+
102
+ try:
103
+ proc = subprocess.run( # noqa: S603 - fixed command plus resolved executable.
104
+ cmd,
105
+ input=_codex_prompt(prompt=prompt, system_prompt=system_prompt),
106
+ capture_output=True,
107
+ text=True,
108
+ timeout=_TIMEOUT_SECONDS,
109
+ check=False,
110
+ env=env,
111
+ )
112
+ if proc.returncode != 0:
113
+ stderr = proc.stderr.strip()
114
+ raise RuntimeError(f"codex CLI exited {proc.returncode}: {stderr[:500]}")
115
+ content = output_path.read_text(encoding="utf-8").strip()
116
+ except subprocess.TimeoutExpired as exc:
117
+ raise TimeoutError(f"codex CLI timed out after {_TIMEOUT_SECONDS}s") from exc
118
+ finally:
119
+ try:
120
+ output_path.unlink()
121
+ except OSError:
122
+ pass
123
+
124
+ if not content:
125
+ raise RuntimeError("codex CLI returned empty output")
126
+ return content
127
+
128
+
129
+ def _temporary_output_path() -> Path:
130
+ handle = tempfile.NamedTemporaryFile(prefix="claude-smart-codex-", delete=False)
131
+ try:
132
+ return Path(handle.name)
133
+ finally:
134
+ handle.close()
135
+
136
+
137
+ def _codex_prompt(*, prompt: str, system_prompt: str) -> str:
138
+ if not system_prompt:
139
+ return prompt
140
+ return f"{system_prompt}\n\n## Task\n{prompt}"
141
+
142
+
143
+ if __name__ == "__main__":
144
+ raise SystemExit(main())
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { spawn, spawnSync } = require("node:child_process");
5
+ const fs = require("node:fs");
6
+ const http = require("node:http");
7
+ const os = require("node:os");
8
+ const path = require("node:path");
9
+
10
+ const HOME = os.homedir();
11
+ const STATE_DIR = path.join(HOME, ".claude-smart");
12
+ const REFLEXIO_DIR = path.join(HOME, ".reflexio");
13
+ const DEFAULT_BACKEND_PORT = 8071;
14
+ const FALLBACK_BACKEND_PORT = 8072;
15
+ const DASHBOARD_PORT = 3001;
16
+
17
+ function emitOk() {
18
+ process.stdout.write('{"continue":true,"suppressOutput":true}\n');
19
+ }
20
+
21
+ function ensureDir(dir) {
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ }
24
+
25
+ function appendLog(name, line) {
26
+ ensureDir(STATE_DIR);
27
+ fs.appendFileSync(path.join(STATE_DIR, name), `${line}\n`);
28
+ }
29
+
30
+ function pluginRoot() {
31
+ for (const value of [process.env.CLAUDE_PLUGIN_ROOT, process.env.PLUGIN_ROOT]) {
32
+ if (value && fs.existsSync(path.join(value, "pyproject.toml"))) {
33
+ return path.resolve(value);
34
+ }
35
+ }
36
+ const fromScript = path.resolve(__dirname, "..");
37
+ if (fs.existsSync(path.join(fromScript, "pyproject.toml"))) return fromScript;
38
+ const cacheRoot = path.join(HOME, ".codex", "plugins", "cache", "reflexioai", "claude-smart");
39
+ try {
40
+ const versions = fs
41
+ .readdirSync(cacheRoot, { withFileTypes: true })
42
+ .filter((entry) => entry.isDirectory())
43
+ .map((entry) => path.join(cacheRoot, entry.name))
44
+ .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
45
+ for (const candidate of versions) {
46
+ if (fs.existsSync(path.join(candidate, "pyproject.toml"))) return candidate;
47
+ }
48
+ } catch {
49
+ // Fall through to the stable plugin-root link.
50
+ }
51
+ return path.join(REFLEXIO_DIR, "plugin-root");
52
+ }
53
+
54
+ function prependRuntimePath() {
55
+ const privateNode = path.join(STATE_DIR, "node", "current");
56
+ const parts = [
57
+ path.join(privateNode, "bin"),
58
+ privateNode,
59
+ path.join(HOME, ".local", "bin"),
60
+ path.join(HOME, ".cargo", "bin"),
61
+ ];
62
+ process.env.PATH = `${parts.join(path.delimiter)}${path.delimiter}${process.env.PATH || ""}`;
63
+ }
64
+
65
+ function commandPath(names) {
66
+ const pathParts = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
67
+ for (const dir of pathParts) {
68
+ for (const name of names) {
69
+ const candidate = path.join(dir, name);
70
+ if (fs.existsSync(candidate)) return candidate;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function uvPath() {
77
+ return commandPath(process.platform === "win32" ? ["uv.exe", "uv"] : ["uv"]);
78
+ }
79
+
80
+ function npmPath() {
81
+ return commandPath(process.platform === "win32" ? ["npm.cmd", "npm.exe", "npm"] : ["npm"]);
82
+ }
83
+
84
+ function stateFile(name) {
85
+ return path.join(STATE_DIR, name);
86
+ }
87
+
88
+ function backendUrlFile() {
89
+ return stateFile("backend-url");
90
+ }
91
+
92
+ function writeBackendUrl(port) {
93
+ ensureDir(STATE_DIR);
94
+ fs.writeFileSync(backendUrlFile(), `http://localhost:${port}/\n`);
95
+ }
96
+
97
+ function codexCompatPath(root) {
98
+ return path.join(root, "scripts", "codex-claude-compat.py");
99
+ }
100
+
101
+ function readBackendUrl() {
102
+ if (process.env.REFLEXIO_URL) return process.env.REFLEXIO_URL;
103
+ try {
104
+ const value = fs.readFileSync(backendUrlFile(), "utf8").trim();
105
+ if (value) return value;
106
+ } catch {
107
+ // Fall through to default.
108
+ }
109
+ return `http://localhost:${DEFAULT_BACKEND_PORT}/`;
110
+ }
111
+
112
+ function healthOk(port, pathname, markerHeader) {
113
+ return new Promise((resolve) => {
114
+ const req = http.request(
115
+ {
116
+ host: "127.0.0.1",
117
+ port,
118
+ path: pathname,
119
+ method: "GET",
120
+ timeout: 1200,
121
+ },
122
+ (res) => {
123
+ const ok = res.statusCode && res.statusCode >= 200 && res.statusCode < 400;
124
+ const markerOk = markerHeader ? Boolean(res.headers[markerHeader]) : true;
125
+ res.resume();
126
+ resolve(Boolean(ok && markerOk));
127
+ },
128
+ );
129
+ req.on("timeout", () => req.destroy());
130
+ req.on("error", () => resolve(false));
131
+ req.end();
132
+ });
133
+ }
134
+
135
+ function portOccupied(port) {
136
+ return new Promise((resolve) => {
137
+ const req = http.request(
138
+ {
139
+ host: "127.0.0.1",
140
+ port,
141
+ path: "/",
142
+ method: "GET",
143
+ timeout: 900,
144
+ },
145
+ (res) => {
146
+ res.resume();
147
+ resolve(true);
148
+ },
149
+ );
150
+ req.on("timeout", () => req.destroy());
151
+ req.on("error", (err) => {
152
+ resolve(err && err.code !== "ECONNREFUSED");
153
+ });
154
+ req.end();
155
+ });
156
+ }
157
+
158
+ async function waitForHealth(port, pathname, markerHeader, attempts) {
159
+ for (let i = 0; i < attempts; i += 1) {
160
+ if (await healthOk(port, pathname, markerHeader)) return true;
161
+ await new Promise((resolve) => setTimeout(resolve, 1000));
162
+ }
163
+ return false;
164
+ }
165
+
166
+ function detached(command, args, options = {}) {
167
+ const child = spawn(command, args, {
168
+ cwd: options.cwd,
169
+ env: options.env || process.env,
170
+ detached: true,
171
+ shell: process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command),
172
+ stdio: "ignore",
173
+ windowsHide: true,
174
+ });
175
+ child.unref();
176
+ return child.pid;
177
+ }
178
+
179
+ function readPid(file) {
180
+ try {
181
+ const value = fs.readFileSync(file, "utf8").trim();
182
+ return value ? Number(value) : null;
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ function pidAlive(pid) {
189
+ if (!pid || Number.isNaN(pid)) return false;
190
+ try {
191
+ process.kill(pid, 0);
192
+ return true;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ function writePid(file, pid) {
199
+ ensureDir(path.dirname(file));
200
+ fs.writeFileSync(file, `${pid}\n`);
201
+ }
202
+
203
+ function ensurePluginRoot(root) {
204
+ ensureDir(REFLEXIO_DIR);
205
+ const link = path.join(REFLEXIO_DIR, "plugin-root");
206
+ try {
207
+ fs.rmSync(link, { recursive: true, force: true });
208
+ } catch {
209
+ // Ignore and try to recreate below.
210
+ }
211
+ try {
212
+ fs.symlinkSync(root, link, process.platform === "win32" ? "junction" : "dir");
213
+ } catch {
214
+ fs.writeFileSync(path.join(REFLEXIO_DIR, "plugin-root.txt"), `${root}\n`);
215
+ }
216
+ }
217
+
218
+ async function startBackend(root) {
219
+ if (process.env.CLAUDE_SMART_BACKEND_AUTOSTART === "0") {
220
+ emitOk();
221
+ return;
222
+ }
223
+ const pidFile = path.join(STATE_DIR, "backend.pid");
224
+ for (const port of [DEFAULT_BACKEND_PORT, FALLBACK_BACKEND_PORT]) {
225
+ if (pidAlive(readPid(pidFile)) && await healthOk(port, "/health")) {
226
+ writeBackendUrl(port);
227
+ emitOk();
228
+ return;
229
+ }
230
+ if (await healthOk(port, "/health")) {
231
+ writeBackendUrl(port);
232
+ emitOk();
233
+ return;
234
+ }
235
+ }
236
+ const uv = uvPath();
237
+ if (!uv) {
238
+ appendLog("backend.log", "[claude-smart] backend: uv not on PATH; skipping");
239
+ emitOk();
240
+ return;
241
+ }
242
+ let selectedPort = DEFAULT_BACKEND_PORT;
243
+ if (await portOccupied(DEFAULT_BACKEND_PORT)) {
244
+ appendLog("backend.log", "[claude-smart] backend: port 8071 occupied; trying 8072");
245
+ selectedPort = FALLBACK_BACKEND_PORT;
246
+ }
247
+ const backendUrl = `http://localhost:${selectedPort}/`;
248
+ const env = {
249
+ ...process.env,
250
+ BACKEND_PORT: String(selectedPort),
251
+ REFLEXIO_URL: backendUrl,
252
+ CLAUDE_SMART_USE_LOCAL_CLI: process.env.CLAUDE_SMART_USE_LOCAL_CLI || "1",
253
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING: process.env.CLAUDE_SMART_USE_LOCAL_EMBEDDING || "1",
254
+ CLAUDE_SMART_HOST: "codex",
255
+ CLAUDE_SMART_CLI_PATH: process.env.CLAUDE_SMART_CLI_PATH || codexCompatPath(root),
256
+ INTERACTION_CLEANUP_THRESHOLD: process.env.INTERACTION_CLEANUP_THRESHOLD || "500",
257
+ INTERACTION_CLEANUP_DELETE_COUNT: process.env.INTERACTION_CLEANUP_DELETE_COUNT || "200",
258
+ };
259
+ const pid = detached(
260
+ uv,
261
+ [
262
+ "run",
263
+ "--project",
264
+ root,
265
+ "--quiet",
266
+ "reflexio",
267
+ "services",
268
+ "start",
269
+ "--only",
270
+ "backend",
271
+ "--no-reload",
272
+ ],
273
+ { cwd: root, env },
274
+ );
275
+ writePid(pidFile, pid);
276
+ if (await waitForHealth(selectedPort, "/health", null, 10)) {
277
+ writeBackendUrl(selectedPort);
278
+ }
279
+ emitOk();
280
+ }
281
+
282
+ async function startDashboard(root) {
283
+ if (process.env.CLAUDE_SMART_DASHBOARD_AUTOSTART === "0") {
284
+ emitOk();
285
+ return;
286
+ }
287
+ const dashboard = path.join(root, "dashboard");
288
+ if (!fs.existsSync(dashboard)) {
289
+ emitOk();
290
+ return;
291
+ }
292
+ const pidFile = path.join(STATE_DIR, "dashboard.pid");
293
+ if (
294
+ pidAlive(readPid(pidFile)) &&
295
+ await healthOk(DASHBOARD_PORT, "/api/health", "x-claude-smart-dashboard")
296
+ ) {
297
+ emitOk();
298
+ return;
299
+ }
300
+ const npm = npmPath();
301
+ if (!npm) {
302
+ appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH; skipping");
303
+ emitOk();
304
+ return;
305
+ }
306
+ if (!fs.existsSync(path.join(dashboard, ".next"))) {
307
+ const buildPidFile = path.join(STATE_DIR, "dashboard-build.pid");
308
+ if (!pidAlive(readPid(buildPidFile))) {
309
+ const pid = detached(npm, ["run", "build"], { cwd: dashboard });
310
+ writePid(buildPidFile, pid);
311
+ appendLog("dashboard.log", "[claude-smart] dashboard: .next missing; started background build");
312
+ }
313
+ emitOk();
314
+ return;
315
+ }
316
+ const env = {
317
+ ...process.env,
318
+ PORT: String(DASHBOARD_PORT),
319
+ REFLEXIO_URL: readBackendUrl(),
320
+ CLAUDE_SMART_DASHBOARD_WORKSPACE: process.cwd(),
321
+ };
322
+ const pid = detached(npm, ["run", "start"], { cwd: dashboard, env });
323
+ writePid(pidFile, pid);
324
+ await waitForHealth(DASHBOARD_PORT, "/api/health", "x-claude-smart-dashboard", 5);
325
+ emitOk();
326
+ }
327
+
328
+ function runHook(root, event) {
329
+ const uv = uvPath();
330
+ if (!uv) {
331
+ appendLog("backend.log", "[claude-smart] hook: uv not on PATH; skipping");
332
+ emitOk();
333
+ return 0;
334
+ }
335
+ const input = fs.readFileSync(0);
336
+ const result = spawnSync(
337
+ uv,
338
+ ["run", "--project", root, "--quiet", "python", "-m", "claude_smart.hook", "codex", event],
339
+ {
340
+ cwd: root,
341
+ env: {
342
+ ...process.env,
343
+ REFLEXIO_URL: readBackendUrl(),
344
+ CLAUDE_SMART_HOST: "codex",
345
+ CLAUDE_SMART_CLI_PATH: process.env.CLAUDE_SMART_CLI_PATH || codexCompatPath(root),
346
+ },
347
+ input,
348
+ stdio: ["pipe", "inherit", "inherit"],
349
+ windowsHide: true,
350
+ },
351
+ );
352
+ return typeof result.status === "number" ? result.status : 1;
353
+ }
354
+
355
+ async function main() {
356
+ prependRuntimePath();
357
+ const root = pluginRoot();
358
+ process.env.PLUGIN_ROOT = root;
359
+ process.env.CLAUDE_PLUGIN_ROOT = root;
360
+ const action = process.argv[2] || "hook";
361
+ if (action === "ensure-root") {
362
+ ensurePluginRoot(root);
363
+ emitOk();
364
+ return 0;
365
+ }
366
+ if (action === "backend") {
367
+ await startBackend(root);
368
+ return 0;
369
+ }
370
+ if (action === "dashboard") {
371
+ await startDashboard(root);
372
+ return 0;
373
+ }
374
+ if (action === "hook") {
375
+ return runHook(root, process.argv[3] || "session-start");
376
+ }
377
+ emitOk();
378
+ return 0;
379
+ }
380
+
381
+ main()
382
+ .then((code) => process.exit(code))
383
+ .catch((err) => {
384
+ appendLog("backend.log", `[claude-smart] codex hook failed: ${err && err.stack ? err.stack : err}`);
385
+ emitOk();
386
+ });
@@ -51,7 +51,8 @@ if [ "$FOLLOW" = "1" ]; then
51
51
  fi
52
52
 
53
53
  # Cache-tracking: if the link currently resolves to a path under the
54
- # managed plugin cache (~/.claude/plugins/cache/), always retarget it to
54
+ # managed plugin cache (~/.claude/plugins/cache/ or ~/.codex/plugins/cache/),
55
+ # always retarget it to
55
56
  # $TARGET. Plugin updates leave old version directories behind, so a
56
57
  # valid pyproject.toml at the stale target is not proof the link is
57
58
  # fresh. Links pointing outside the cache (e.g., a user's local-dev
@@ -60,7 +61,7 @@ if [ -L "$LINK" ]; then
60
61
  # Literal target string, not realpath: we compare against what was written by ln -s.
61
62
  CURRENT="$(readlink "$LINK" 2>/dev/null || true)"
62
63
  case "$CURRENT" in
63
- "$HOME/.claude/plugins/cache/"*)
64
+ "$HOME/.claude/plugins/cache/"*|"$HOME/.codex/plugins/cache/"*)
64
65
  CURRENT_NORM="${CURRENT%/}"
65
66
  TARGET_NORM="${TARGET%/}"
66
67
  if [ "$CURRENT_NORM" != "$TARGET_NORM" ]; then
@@ -311,7 +311,6 @@ if ! grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
311
311
  printf '# Use the in-process ONNX embedder (chromadb) — no API key for semantic search\nCLAUDE_SMART_USE_LOCAL_EMBEDDING=1\n' >> "$REFLEXIO_ENV"
312
312
  echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
313
313
  fi
314
-
315
314
  # Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
316
315
  # plugin backend port (8071). Matches the quoted and unquoted forms but
317
316
  # requires paired quotes, so malformed or deliberately different values