Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions bin/claude-smart.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,14 +508,14 @@ function runChecked(command, args, options = {}) {
});
}

function runPluginService(pluginRoot, scriptName, subcommand) {
function runPluginService(pluginRoot, scriptName, subcommand, envOverrides = {}) {
const script = join(pluginRoot, "scripts", scriptName);
if (!existsSync(script)) return false;
const bash = resolveCommand(isWindows() ? ["bash.exe", "bash"] : ["bash"]);
if (!bash) return false;
const result = spawnSync(bash, [script, subcommand], {
cwd: pluginRoot,
env: runtimeEnv(),
env: { ...runtimeEnv(), ...envOverrides },
stdio: "ignore",
windowsHide: true,
timeout: PLUGIN_SERVICE_TIMEOUT_MS,
Expand All @@ -542,6 +542,12 @@ function refreshDashboardService(pluginRoot) {
return runPluginService(pluginRoot, "dashboard-service.sh", "start");
}

function startBackendService(pluginRoot, host) {
return runPluginService(pluginRoot, "backend-service.sh", "start", {
CLAUDE_SMART_HOST: host,
});
}

function stopClaudeSmartServices(pluginRoot) {
runPluginService(pluginRoot, "dashboard-service.sh", "stop");
runPluginService(pluginRoot, "backend-service.sh", "stop");
Expand Down Expand Up @@ -834,9 +840,7 @@ function patchCodexHooksForNode(pluginRoot, nodePath) {
const runner = join(pluginRoot, "scripts", "codex-hook.js");
const command = (...args) => [nodePath, runner, ...args].map(quoteCommandPart).join(" ");
// Dispatch by command content rather than index — entries can be added or
// reordered (e.g. the SessionStart install hook at index 0) without
// breaking the patch. Entries that must run as bash (smart-install.sh)
// are left untouched.
// reordered without breaking the patch.
const patchOne = (original) => {
if (typeof original !== "string") return original;
if (original.includes("smart-install.sh")) return original;
Expand Down Expand Up @@ -1512,6 +1516,9 @@ async function runInstall(args) {
process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
}
process.stdout.write(`Prepared claude-smart runtime at ${pluginRoot}.\n`);
if (startBackendService(pluginRoot, "claude-code")) {
process.stdout.write("Started claude-smart backend service.\n");
}
if (refreshDashboardService(pluginRoot)) {
process.stdout.write("Refreshed claude-smart dashboard service.\n");
}
Expand Down Expand Up @@ -1592,6 +1599,9 @@ async function runInstallCodex(args) {
if (readOnly) {
process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
}
if (startBackendService(cacheDir, "codex")) {
process.stdout.write("Started claude-smart backend service.\n");
}
if (refreshDashboardService(cacheDir)) {
process.stdout.write("Refreshed claude-smart dashboard service.\n");
}
Expand Down
5 changes: 0 additions & 5 deletions plugin/hooks/codex-hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"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/smart-install.sh\" || true",
"timeout": 300
},
{
"type": "command",
"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",
Expand Down
10 changes: 0 additions & 10 deletions plugin/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@
}
],
"SessionStart": [
{
"matcher": "startup|clear|compact|resume",
"hooks": [
{
"type": "command",
"command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/reflexioai/plugin\"; bash \"$_R/scripts/smart-install.sh\"",
"timeout": 300
}
]
},
{
"matcher": "startup|clear|compact|resume",
"hooks": [
Expand Down
10 changes: 5 additions & 5 deletions plugin/scripts/backend-service.sh
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,13 @@ case "$CMD" in
fi
if ! command -v uv >/dev/null 2>&1; then
if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; running installer"
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true
claude_smart_source_login_path
claude_smart_prepend_astral_bins
claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; starting installer in background"
claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
>>"$STATE_DIR/install.log" 2>&1 || true
fi
if ! command -v uv >/dev/null 2>&1; then
claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH after installer; skipping"
claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; installer recovery scheduled; skipping"
emit_ok; exit 0
fi
fi
Expand Down
45 changes: 7 additions & 38 deletions plugin/scripts/codex-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,33 +302,6 @@ function detached(command, args, options = {}) {
return child.pid;
}

function runInstaller(root, reason) {
if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false;
const script = path.join(root, "scripts", "smart-install.sh");
if (!fs.existsSync(script)) return false;
const bash = bashPath();
if (!bash) return false;
appendLog("backend.log", `[claude-smart] ${reason}; running installer`);
const result = spawnSync(bash, [script], {
cwd: root,
env: {
...process.env,
CLAUDE_SMART_BOOTSTRAPPING: "1",
},
encoding: "utf8",
maxBuffer: 20 * 1024 * 1024,
windowsHide: true,
});
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
if (output) {
ensureDir(STATE_DIR);
fs.appendFileSync(path.join(STATE_DIR, "install.log"), `${output}\n`);
trimLog(path.join(STATE_DIR, "install.log"));
}
prependRuntimePath();
return result.status === 0;
}

function startInstallerDetached(root, reason) {
if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false;
const script = path.join(root, "scripts", "smart-install.sh");
Expand Down Expand Up @@ -410,11 +383,11 @@ async function startBackend(root) {
}
const uv = uvPath();
if (!uv) {
runInstaller(root, "backend: uv not on PATH");
startInstallerDetached(root, "backend: uv not on PATH");
}
const readyUv = uvPath();
if (!readyUv) {
appendLog("backend.log", "[claude-smart] backend: uv not on PATH after installer; skipping");
appendLog("backend.log", "[claude-smart] backend: uv not on PATH; installer recovery scheduled; skipping");
emitOk();
return;
}
Expand Down Expand Up @@ -478,11 +451,11 @@ async function startDashboard(root) {
}
const npm = npmPath();
if (!npm) {
runInstaller(root, "dashboard: npm not on PATH");
startInstallerDetached(root, "dashboard: npm not on PATH");
}
const readyNpm = npmPath();
if (!readyNpm) {
appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH after installer; skipping");
appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH; installer recovery scheduled; skipping");
emitOk();
return;
}
Expand Down Expand Up @@ -512,14 +485,10 @@ function runHook(root, event) {
trimLog(path.join(STATE_DIR, "backend.log"));
let uv = uvPath();
if (!uv) {
if (event === "session-start") {
runInstaller(root, "hook: uv not on PATH");
uv = uvPath();
} else {
startInstallerDetached(root, "hook: uv not on PATH");
}
startInstallerDetached(root, "hook: uv not on PATH");
uv = uvPath();
if (!uv) {
appendLog("backend.log", "[claude-smart] hook: uv not on PATH after installer; skipping");
appendLog("backend.log", "[claude-smart] hook: uv not on PATH; installer recovery scheduled; skipping");
emitHookOk();
return 0;
}
Expand Down
11 changes: 5 additions & 6 deletions plugin/scripts/dashboard-service.sh
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,13 @@ case "$CMD" in
NPM_BIN=$(claude_smart_resolve_npm || true)
if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
echo "[claude-smart] dashboard: npm is not on PATH; running installer" >>"$LOG_FILE"
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true
claude_smart_source_login_path
claude_smart_prepend_node_bins
NPM_BIN=$(claude_smart_resolve_npm || true)
echo "[claude-smart] dashboard: npm is not on PATH; starting installer in background" >>"$LOG_FILE"
claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
>>"$STATE_DIR/install.log" 2>&1 || true
fi
if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
reason="npm is not on PATH after installer; dashboard cannot start"
reason="npm is not on PATH; installer recovery scheduled; dashboard cannot start yet"
echo "[claude-smart] dashboard: $reason; skipping" >>"$LOG_FILE"
claude_smart_write_dashboard_unavailable "$reason"
emit_ok; exit 0
Expand Down
41 changes: 13 additions & 28 deletions plugin/scripts/hook_entry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,29 +85,18 @@ PY
fi

if ! command -v uv >/dev/null 2>&1; then
# Self-heal from skipped Setup/SessionStart bootstrap. SessionStart can
# afford to wait because it has the install budget; prompt/tool hooks start
# the same installer detached so normal work is not blocked by first-run
# dependency setup.
# Self-heal from skipped setup without blocking the first prompt. The
# installer and Setup hook own dependency bootstrap; hooks only schedule
# recovery and return so SessionStart stays lightweight.
if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then
claude_smart_emit_continue
exit 0
fi
if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
mkdir -p "$STATE_DIR"
if [ "$EVENT" = "session-start" ]; then
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2
claude_smart_prepend_astral_bins
claude_smart_prepend_node_bins
if command -v uv >/dev/null 2>&1; then
bash "$HERE/backend-service.sh" start >/dev/null 2>&1 || true
bash "$HERE/dashboard-service.sh" start >/dev/null 2>&1 || true
fi
else
claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
>>"$STATE_DIR/install.log" 2>&1 || true
fi
claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
>>"$STATE_DIR/install.log" 2>&1 || true
fi
if ! command -v uv >/dev/null 2>&1; then
claude_smart_emit_continue
Expand All @@ -128,17 +117,13 @@ ensure_hook_package_importable() {
fi

mkdir -p "$STATE_DIR"
if [ "$EVENT" = "session-start" ]; then
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2
else
{
printf '%s\n' "[claude-smart] hook: claude_smart.hook is not importable; retrying install in background"
date 2>/dev/null || true
} >>"$STATE_DIR/install.log" 2>&1
claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
>>"$STATE_DIR/install.log" 2>&1 || true
fi
{
printf '%s\n' "[claude-smart] hook: claude_smart.hook is not importable; retrying install in background"
date 2>/dev/null || true
} >>"$STATE_DIR/install.log" 2>&1
claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
>>"$STATE_DIR/install.log" 2>&1 || true

claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.hook
}
Expand Down
12 changes: 11 additions & 1 deletion plugin/scripts/smart-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ install_complete() {
return 0
}

start_backend_service() {
if [ -x "$HERE/backend-service.sh" ]; then
echo "[claude-smart] starting backend service in background" >&2
bash "$HERE/backend-service.sh" start >/dev/null 2>&1 || true
fi
}

install_vendored_reflexio() {
local VENDORED_REFLEXIO PLUGIN_PYTHON

Expand Down Expand Up @@ -391,6 +398,7 @@ fi
preflight_supported_runtime_platform

if install_complete; then
start_backend_service
claude_smart_emit_continue
exit 0
fi
Expand Down Expand Up @@ -624,6 +632,8 @@ if ! bash "$HERE/ensure-plugin-root.sh" "$PLUGIN_ROOT"; then
echo "[claude-smart] WARNING: failed to set ~/.reflexio/plugin-root symlink — slash commands may not resolve" >&2
fi

start_backend_service

write_success_marker
echo "[claude-smart] install complete. Backend and dashboard auto-start on session start." >&2
echo "[claude-smart] install complete. Backend started; dashboard auto-starts on session start." >&2
claude_smart_emit_continue
5 changes: 4 additions & 1 deletion tests/test_codex_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,10 @@ def fake_run_codex(args: list[str]):
rc = cli.cmd_install(argparse.Namespace(host="codex", source="unused"))

assert rc == 0
assert not env_path.exists()
env_text = env_path.read_text()
assert "CLAUDE_SMART_USE_LOCAL_CLI=1" in env_text
assert "CLAUDE_SMART_USE_LOCAL_EMBEDDING=1" in env_text
assert 'CLAUDE_SMART_READ_ONLY="0"' in env_text
config = config_path.read_text()
assert "plugin_hooks = true" in config
assert '[plugins."claude-smart@reflexioai"]' in config
Expand Down
Loading
Loading