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.
- package/README.md +30 -53
- package/bin/claude-smart.js +516 -11
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +2 -1
- package/plugin/hooks/codex-hooks.json +7 -7
- package/plugin/pyproject.toml +2 -1
- package/plugin/scripts/_codex_env.sh +1 -0
- package/plugin/scripts/backend-service.sh +12 -7
- package/plugin/scripts/codex-claude-compat.py +144 -0
- package/plugin/scripts/codex-hook.js +386 -0
- package/plugin/scripts/ensure-plugin-root.sh +3 -2
- package/plugin/scripts/smart-install.sh +0 -1
- package/plugin/skills/claude-smart/SKILL.md +32 -0
- package/plugin/src/claude_smart/cli.py +234 -17
- package/plugin/src/claude_smart/events/stop.py +16 -1
- package/plugin/src/claude_smart/internal_call.py +30 -0
- package/plugin/src/claude_smart/optimizer_assistant.py +86 -6
- package/plugin/uv.lock +12 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
]
|
package/plugin/pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "claude-smart"
|
|
3
|
-
version = "0.2.
|
|
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]
|
|
@@ -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
|
|
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
|
|
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
|
|
42
|
-
|
|
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
|
|
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/),
|
|
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
|