claude-smart 0.2.28 → 0.2.29

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.
@@ -14,6 +14,16 @@
14
14
  }
15
15
  ],
16
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/smart-install.sh\"",
23
+ "timeout": 300
24
+ }
25
+ ]
26
+ },
17
27
  {
18
28
  "matcher": "startup|clear|compact|resume",
19
29
  "hooks": [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.28"
3
+ version = "0.2.29"
4
4
  description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -233,6 +233,44 @@ claude_smart_npm_available() {
233
233
  "$npm_bin" --version >/dev/null 2>&1
234
234
  }
235
235
 
236
+ claude_smart_log_max_bytes() {
237
+ printf '%s\n' "10000000"
238
+ }
239
+
240
+ claude_smart_trim_log_file() {
241
+ local file max_bytes size tmp
242
+ file="$1"
243
+ max_bytes="${2:-$(claude_smart_log_max_bytes)}"
244
+ case "$max_bytes" in
245
+ ''|*[!0-9]*) return 0 ;;
246
+ esac
247
+ [ -f "$file" ] || return 0
248
+ size=$(wc -c < "$file" 2>/dev/null | tr -d '[:space:]') || return 0
249
+ case "$size" in
250
+ ''|*[!0-9]*) return 0 ;;
251
+ esac
252
+ [ "$size" -le "$max_bytes" ] && return 0
253
+
254
+ tmp="${file}.trim.$$"
255
+ if tail -c "$max_bytes" "$file" > "$tmp" 2>/dev/null; then
256
+ # Rewrite the existing path instead of replacing it, so a process with
257
+ # this file already open in append mode keeps writing to the capped file.
258
+ cat "$tmp" > "$file"
259
+ fi
260
+ rm -f "$tmp"
261
+ }
262
+
263
+ claude_smart_append_capped_log() {
264
+ local file max_bytes
265
+ file="$1"
266
+ max_bytes="${2:-$(claude_smart_log_max_bytes)}"
267
+ shift 2
268
+ mkdir -p "$(dirname "$file")"
269
+ claude_smart_trim_log_file "$file" "$max_bytes"
270
+ printf '%s\n' "$*" >> "$file"
271
+ claude_smart_trim_log_file "$file" "$max_bytes"
272
+ }
273
+
236
274
  # Spawn a command fully detached from the current shell so a hook timeout
237
275
  # (Claude Code's install/SessionStart budget) cannot kill it mid-flight.
238
276
  # POSIX: setsid → python3 os.setsid → nohup (in that order of strength).
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # Run the backend command and stream its stdout/stderr into a capped log file.
3
+ set -eu
4
+
5
+ HERE="$(cd "$(dirname "$0")" && pwd)"
6
+ # shellcheck source=_lib.sh
7
+ . "$HERE/_lib.sh"
8
+
9
+ if [ "$#" -lt 4 ] || [ "$3" != "--" ]; then
10
+ exit 2
11
+ fi
12
+
13
+ LOG_FILE="$1"
14
+ MAX_BYTES="$2"
15
+ shift 3
16
+
17
+ mkdir -p "$(dirname "$LOG_FILE")"
18
+ claude_smart_trim_log_file "$LOG_FILE" "$MAX_BYTES"
19
+
20
+ STATUS_FILE="${TMPDIR:-/tmp}/claude-smart-backend-log-runner.$$.status"
21
+ rm -f "$STATUS_FILE"
22
+
23
+ (
24
+ set +e
25
+ "$@" 2>&1
26
+ printf '%s\n' "$?" > "$STATUS_FILE"
27
+ ) | while IFS= read -r line || [ -n "$line" ]; do
28
+ claude_smart_append_capped_log "$LOG_FILE" "$MAX_BYTES" "$line"
29
+ done
30
+
31
+ status=$(cat "$STATUS_FILE" 2>/dev/null || printf '1')
32
+ rm -f "$STATUS_FILE"
33
+ exit "$status"
@@ -41,9 +41,11 @@ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
41
41
 
42
42
  if [ -z "${CLAUDE_SMART_CLI_PATH:-}" ]; then
43
43
  if [ "${CLAUDE_SMART_HOST:-claude-code}" = "codex" ]; then
44
- # Prefer the compatibility executable for older provider entrypoints; the
45
- # provider can also resolve `codex` directly when this override is absent.
46
- export CLAUDE_SMART_CLI_PATH="$PLUGIN_ROOT/scripts/codex-claude-compat.py"
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
+ claude_smart_prepend_node_bins
48
+ export CLAUDE_SMART_CLI_PATH="$PLUGIN_ROOT/scripts/codex-claude-compat"
47
49
  elif _cs_cli_path=$(command -v claude 2>/dev/null) && [ -n "$_cs_cli_path" ]; then
48
50
  export CLAUDE_SMART_CLI_PATH="$_cs_cli_path"
49
51
  elif [ -x "$HOME/.local/bin/claude" ]; then
@@ -55,7 +57,9 @@ fi
55
57
  STATE_DIR="$HOME/.claude-smart"
56
58
  PID_FILE="$STATE_DIR/backend.pid"
57
59
  LOG_FILE="$STATE_DIR/backend.log"
60
+ LOG_MAX_BYTES="$(claude_smart_log_max_bytes)"
58
61
  mkdir -p "$STATE_DIR"
62
+ claude_smart_trim_log_file "$LOG_FILE" "$LOG_MAX_BYTES"
59
63
 
60
64
  emit_ok() { echo '{"continue":true,"suppressOutput":true}'; }
61
65
 
@@ -178,11 +182,11 @@ case "$CMD" in
178
182
  if port_occupied; then
179
183
  # Something answered the TCP probe but /health didn't — don't
180
184
  # start a second uvicorn on top of it.
181
- echo "[claude-smart] backend: port $PORT held by another process; skipping" >>"$LOG_FILE"
185
+ claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: port $PORT held by another process; skipping"
182
186
  emit_ok; exit 0
183
187
  fi
184
188
  if ! command -v uv >/dev/null 2>&1; then
185
- echo "[claude-smart] backend: uv not on PATH; skipping" >>"$LOG_FILE"
189
+ claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; skipping"
186
190
  emit_ok; exit 0
187
191
  fi
188
192
  cd "$PLUGIN_ROOT"
@@ -198,12 +202,12 @@ case "$CMD" in
198
202
  # bookkeeping harder and we don't need hot-reload for a user-facing
199
203
  # service. Detach via claude_smart_spawn_detached so the same code
200
204
  # path covers Linux (setsid), macOS (python3 os.setsid), and Windows
201
- # (nohup; no process groups). Caller-side stdout/stderr redirection
202
- # works across all three primitives Git Bash routes the > and 2>&1
203
- # through to the underlying CRT before nohup execs the child.
204
- claude_smart_spawn_detached uv run --project "$PLUGIN_ROOT" --quiet \
205
- reflexio services start --only backend --no-reload \
206
- >>"$LOG_FILE" 2>&1
205
+ # (nohup; no process groups). backend-log-runner.sh owns stdout/stderr
206
+ # capture so process output cannot grow backend.log past its cap.
207
+ claude_smart_spawn_detached bash "$HERE/backend-log-runner.sh" \
208
+ "$LOG_FILE" "$LOG_MAX_BYTES" -- \
209
+ uv run --project "$PLUGIN_ROOT" --quiet \
210
+ reflexio services start --only backend --no-reload
207
211
  svc_pid=$!
208
212
  # Record the spawned pid, not a pgid sampled with ps. On POSIX,
209
213
  # setsid/python os.setsid make this pid the new process group leader;
@@ -32,9 +32,33 @@ if [ -f "$FAILURE_MARKER" ]; then
32
32
  fi
33
33
 
34
34
  if ! command -v uv >/dev/null 2>&1; then
35
- echo "claude-smart: 'uv' not found on PATH." >&2
36
- echo "Install it from https://docs.astral.sh/uv/ or restart Claude Code so the Setup hook can install it." >&2
37
- exit 1
35
+ # Self-heal: the Setup/SessionStart hook may have been skipped (trust
36
+ # prompt declined, plugin enabled mid-session, etc.) leaving the install
37
+ # half-done. Run smart-install.sh inline so the user does not have to
38
+ # restart Claude Code just to recover.
39
+ # Guard against recursion: if smart-install.sh ever shells back through
40
+ # this wrapper (e.g. a future migration step) we must not loop forever.
41
+ if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then
42
+ echo "claude-smart: bootstrap recursion detected; aborting." >&2
43
+ exit 1
44
+ fi
45
+ if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
46
+ echo "claude-smart: 'uv' not found — bootstrapping dependencies (~1-3 min on first install)..." >&2
47
+ CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2
48
+ claude_smart_prepend_astral_bins
49
+ claude_smart_prepend_node_bins
50
+ fi
51
+ if ! command -v uv >/dev/null 2>&1; then
52
+ if [ -f "$FAILURE_MARKER" ]; then
53
+ msg="$(cat "$FAILURE_MARKER" 2>/dev/null || echo "unknown error")"
54
+ echo "claude-smart: install failed: $msg" >&2
55
+ echo "Fix the underlying issue and delete $FAILURE_MARKER to retry." >&2
56
+ else
57
+ echo "claude-smart: 'uv' not found on PATH after bootstrap attempt." >&2
58
+ echo "Install it from https://docs.astral.sh/uv/ or rerun $PLUGIN_ROOT/scripts/smart-install.sh manually." >&2
59
+ fi
60
+ exit 1
61
+ fi
38
62
  fi
39
63
 
40
64
  exec uv run --project "$PLUGIN_ROOT" --quiet python -m claude_smart.cli "$@"
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ case "$0" in
5
+ */*) SCRIPT_DIR=${0%/*} ;;
6
+ *) SCRIPT_DIR=. ;;
7
+ esac
8
+ SCRIPT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR" && pwd)
9
+ exec node "$SCRIPT_DIR/codex-claude-compat.js" "$@"
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ setlocal
3
+ set "SCRIPT_DIR=%~dp0"
4
+ node "%SCRIPT_DIR%codex-claude-compat.js" %*
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /*
5
+ * Translate Reflexio's Claude CLI provider contract to `codex exec`.
6
+ *
7
+ * Reflexio shells out to CLAUDE_SMART_CLI_PATH as if it were Claude Code:
8
+ *
9
+ * <path> -p --output-format stream-json --model <model> ...
10
+ *
11
+ * Under Codex, this small bridge preserves that executable contract while
12
+ * routing the actual generation through the authenticated Codex CLI.
13
+ */
14
+
15
+ const { spawnSync } = require("node:child_process");
16
+ const crypto = require("node:crypto");
17
+ const fs = require("node:fs");
18
+ const os = require("node:os");
19
+ const path = require("node:path");
20
+
21
+ const TIMEOUT_MS = 120_000;
22
+
23
+ function main(argv) {
24
+ try {
25
+ const { outputFormat, systemPrompt } = parseSupportedArgs(argv);
26
+ const content = runCodex({
27
+ prompt: fs.readFileSync(0, "utf8"),
28
+ systemPrompt,
29
+ });
30
+ const payload =
31
+ outputFormat === "stream-json"
32
+ ? { type: "result", subtype: "success", result: content }
33
+ : { result: content };
34
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
35
+ return 0;
36
+ } catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ process.stderr.write(`codex-claude-compat: ${message}\n`);
39
+ return 1;
40
+ }
41
+ }
42
+
43
+ function parseSupportedArgs(argv) {
44
+ let outputFormat = "json";
45
+ let systemPrompt = "";
46
+ let idx = 0;
47
+ while (idx < argv.length) {
48
+ const arg = argv[idx];
49
+ if (arg === "-p") {
50
+ idx += 1;
51
+ } else if (arg === "--output-format") {
52
+ outputFormat = requireValue(argv, idx, arg);
53
+ idx += 2;
54
+ } else if (arg === "--model") {
55
+ requireValue(argv, idx, arg);
56
+ idx += 2;
57
+ } else if (arg === "--verbose" || arg === "--include-partial-messages") {
58
+ idx += 1;
59
+ } else if (arg === "--append-system-prompt") {
60
+ systemPrompt = requireValue(argv, idx, arg);
61
+ idx += 2;
62
+ } else {
63
+ throw new Error(`unsupported Claude CLI argument: ${arg}`);
64
+ }
65
+ }
66
+ if (outputFormat !== "json" && outputFormat !== "stream-json") {
67
+ throw new Error(`unsupported --output-format: ${outputFormat}`);
68
+ }
69
+ return { outputFormat, systemPrompt };
70
+ }
71
+
72
+ function requireValue(argv, idx, name) {
73
+ if (idx + 1 >= argv.length) {
74
+ throw new Error(`${name} requires a value`);
75
+ }
76
+ return argv[idx + 1];
77
+ }
78
+
79
+ function runCodex({ prompt, systemPrompt }) {
80
+ const codexPath = process.env.CLAUDE_SMART_CODEX_PATH || commandPath(codexNames());
81
+ if (!codexPath) {
82
+ throw new Error("codex CLI not found on PATH");
83
+ }
84
+
85
+ const outputPath = temporaryOutputPath();
86
+ const args = [
87
+ "exec",
88
+ "--sandbox",
89
+ "read-only",
90
+ "--skip-git-repo-check",
91
+ "--ephemeral",
92
+ "--ignore-rules",
93
+ "--output-last-message",
94
+ outputPath,
95
+ "-",
96
+ ];
97
+ const env = {
98
+ ...process.env,
99
+ CLAUDE_SMART_HOST: "codex",
100
+ CLAUDE_SMART_INTERNAL: "1",
101
+ CLAUDE_CODE_ENTRYPOINT: "optimizer",
102
+ };
103
+
104
+ try {
105
+ const proc = spawnSync(codexPath, args, {
106
+ input: codexPrompt({ prompt, systemPrompt }),
107
+ encoding: "utf8",
108
+ env,
109
+ timeout: TIMEOUT_MS,
110
+ windowsHide: true,
111
+ shell: process.platform === "win32" && /\.(?:cmd|bat)$/i.test(codexPath),
112
+ });
113
+ if (proc.error) {
114
+ if (proc.error.code === "ETIMEDOUT") {
115
+ throw new Error(`codex CLI timed out after ${TIMEOUT_MS / 1000}s`);
116
+ }
117
+ throw proc.error;
118
+ }
119
+ if (proc.status !== 0) {
120
+ const stderr = String(proc.stderr || "").trim().slice(0, 500);
121
+ throw new Error(`codex CLI exited ${proc.status}: ${stderr}`);
122
+ }
123
+ const content = fs.readFileSync(outputPath, "utf8").trim();
124
+ if (!content) {
125
+ throw new Error("codex CLI returned empty output");
126
+ }
127
+ return content;
128
+ } finally {
129
+ try {
130
+ fs.unlinkSync(outputPath);
131
+ } catch {
132
+ // Best effort cleanup only.
133
+ }
134
+ }
135
+ }
136
+
137
+ function codexNames() {
138
+ return process.platform === "win32" ? ["codex.cmd", "codex.exe", "codex"] : ["codex"];
139
+ }
140
+
141
+ function commandPath(names) {
142
+ const pathParts = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
143
+ for (const dir of pathParts) {
144
+ for (const name of names) {
145
+ const candidate = path.join(dir, name);
146
+ if (fs.existsSync(candidate)) return candidate;
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ function temporaryOutputPath() {
153
+ const suffix = crypto.randomBytes(8).toString("hex");
154
+ return path.join(os.tmpdir(), `claude-smart-codex-${process.pid}-${Date.now()}-${suffix}`);
155
+ }
156
+
157
+ function codexPrompt({ prompt, systemPrompt }) {
158
+ if (!systemPrompt) return prompt;
159
+ return `${systemPrompt}\n\n## Task\n${prompt}`;
160
+ }
161
+
162
+ process.exitCode = main(process.argv.slice(2));
@@ -13,6 +13,7 @@ const REFLEXIO_DIR = path.join(HOME, ".reflexio");
13
13
  const DEFAULT_BACKEND_PORT = 8071;
14
14
  const FALLBACK_BACKEND_PORT = 8072;
15
15
  const DASHBOARD_PORT = 3001;
16
+ const LOG_MAX_BYTES = 10000000;
16
17
 
17
18
  function emitOk() {
18
19
  process.stdout.write('{"continue":true,"suppressOutput":true}\n');
@@ -22,9 +23,31 @@ function ensureDir(dir) {
22
23
  fs.mkdirSync(dir, { recursive: true });
23
24
  }
24
25
 
26
+ function trimLog(file) {
27
+ if (!Number.isFinite(LOG_MAX_BYTES) || LOG_MAX_BYTES < 1) return;
28
+ let stat;
29
+ try {
30
+ stat = fs.statSync(file);
31
+ } catch {
32
+ return;
33
+ }
34
+ if (!stat.isFile() || stat.size <= LOG_MAX_BYTES) return;
35
+ const fd = fs.openSync(file, "r");
36
+ try {
37
+ const buffer = Buffer.alloc(LOG_MAX_BYTES);
38
+ fs.readSync(fd, buffer, 0, LOG_MAX_BYTES, stat.size - LOG_MAX_BYTES);
39
+ fs.writeFileSync(file, buffer);
40
+ } finally {
41
+ fs.closeSync(fd);
42
+ }
43
+ }
44
+
25
45
  function appendLog(name, line) {
26
46
  ensureDir(STATE_DIR);
27
- fs.appendFileSync(path.join(STATE_DIR, name), `${line}\n`);
47
+ const file = path.join(STATE_DIR, name);
48
+ trimLog(file);
49
+ fs.appendFileSync(file, `${line}\n`);
50
+ trimLog(file);
28
51
  }
29
52
 
30
53
  function pluginRoot() {
@@ -95,7 +118,10 @@ function writeBackendUrl(port) {
95
118
  }
96
119
 
97
120
  function codexCompatPath(root) {
98
- return path.join(root, "scripts", "codex-claude-compat.py");
121
+ const filename = process.platform === "win32"
122
+ ? "codex-claude-compat.cmd"
123
+ : "codex-claude-compat";
124
+ return path.join(root, "scripts", filename);
99
125
  }
100
126
 
101
127
  function readBackendUrl() {
@@ -216,6 +242,7 @@ function ensurePluginRoot(root) {
216
242
  }
217
243
 
218
244
  async function startBackend(root) {
245
+ trimLog(path.join(STATE_DIR, "backend.log"));
219
246
  if (process.env.CLAUDE_SMART_BACKEND_AUTOSTART === "0") {
220
247
  emitOk();
221
248
  return;
@@ -326,6 +353,7 @@ async function startDashboard(root) {
326
353
  }
327
354
 
328
355
  function runHook(root, event) {
356
+ trimLog(path.join(STATE_DIR, "backend.log"));
329
357
  const uv = uvPath();
330
358
  if (!uv) {
331
359
  appendLog("backend.log", "[claude-smart] hook: uv not on PATH; skipping");
@@ -20,18 +20,121 @@ REPO_ROOT="$(cd "$HERE/../.." && pwd)"
20
20
 
21
21
  MARKER_DIR="$HOME/.claude-smart"
22
22
  FAILURE_MARKER="$MARKER_DIR/install-failed"
23
+ SUCCESS_MARKER="$MARKER_DIR/install-complete"
24
+ INSTALL_LOCK="$MARKER_DIR/install.lock"
23
25
  mkdir -p "$MARKER_DIR"
26
+
27
+ # Serialize concurrent installer runs (SessionStart hook + slash-command
28
+ # self-heal can both invoke this script). Wait for the active installer
29
+ # rather than returning early, otherwise callers can re-check uv before
30
+ # the first install has finished and report a false missing-dependency error.
31
+ if command -v flock >/dev/null 2>&1; then
32
+ exec 9>"$INSTALL_LOCK"
33
+ if ! flock 9; then
34
+ echo "[claude-smart] install lock failed; continuing without serialization" >&2
35
+ echo '{"continue":true,"suppressOutput":true}'
36
+ exit 0
37
+ fi
38
+ fi
39
+
24
40
  rm -f "$FAILURE_MARKER"
25
41
 
26
42
  write_failure() {
27
43
  local reason
28
44
  reason="$1"
29
45
  printf '%s\n' "$reason" > "$FAILURE_MARKER"
46
+ rm -f "$SUCCESS_MARKER"
30
47
  echo "[claude-smart] install failed: $reason" >&2
31
48
  echo '{"continue":true,"suppressOutput":true}'
32
49
  exit 0
33
50
  }
34
51
 
52
+ fingerprint_file() {
53
+ local path
54
+ path="$1"
55
+ if [ -f "$path" ]; then
56
+ cksum "$path" 2>/dev/null | awk '{print $1 ":" $2}'
57
+ else
58
+ printf 'missing\n'
59
+ fi
60
+ }
61
+
62
+ install_fingerprint() {
63
+ printf 'plugin_root=%s\n' "$PLUGIN_ROOT"
64
+ printf 'smart_install=%s\n' "$(fingerprint_file "$HERE/smart-install.sh")"
65
+ printf 'pyproject=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/pyproject.toml")"
66
+ printf 'uv_lock=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/uv.lock")"
67
+ # Resolved python interpreter — catches a system upgrade (3.12.4 → 3.12.5)
68
+ # that would otherwise let install_complete return true against a venv
69
+ # built against a now-deleted interpreter.
70
+ if command -v uv >/dev/null 2>&1; then
71
+ printf 'python=%s\n' "$(uv python find 3.12 2>/dev/null || echo missing)"
72
+ else
73
+ printf 'python=no-uv\n'
74
+ fi
75
+ if [ -d "$PLUGIN_ROOT/dashboard" ]; then
76
+ printf 'dashboard_pkg=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/dashboard/package.json")"
77
+ printf 'dashboard_lock=%s\n' "$(fingerprint_file "$PLUGIN_ROOT/dashboard/package-lock.json")"
78
+ else
79
+ printf 'dashboard_pkg=none\n'
80
+ printf 'dashboard_lock=none\n'
81
+ fi
82
+ }
83
+
84
+ install_complete() {
85
+ [ -f "$SUCCESS_MARKER" ] || return 1
86
+ [ "$(cat "$SUCCESS_MARKER" 2>/dev/null || true)" = "$(install_fingerprint)" ] || return 1
87
+ command -v uv >/dev/null 2>&1 || return 1
88
+ [ -d "$PLUGIN_ROOT/.venv" ] || return 1
89
+ [ -f "$HOME/.reflexio/.env" ] || return 1
90
+ grep -q '^CLAUDE_SMART_USE_LOCAL_CLI=' "$HOME/.reflexio/.env" || return 1
91
+ grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$HOME/.reflexio/.env" || return 1
92
+ if [ -d "$PLUGIN_ROOT/dashboard" ]; then
93
+ [ -d "$PLUGIN_ROOT/dashboard/.next" ] || [ -f "$MARKER_DIR/dashboard-build.pid" ] || [ -f "$(claude_smart_dashboard_unavailable_marker)" ] || return 1
94
+ fi
95
+ return 0
96
+ }
97
+
98
+ write_success_marker() {
99
+ install_fingerprint > "$SUCCESS_MARKER"
100
+ }
101
+
102
+ preflight_supported_runtime_platform() {
103
+ local os_name machine darwin_major
104
+ os_name="$(uname -s 2>/dev/null || echo unknown)"
105
+ machine="$(uname -m 2>/dev/null || echo unknown)"
106
+ case "$os_name" in
107
+ Darwin*)
108
+ if [ "$machine" != "arm64" ]; then
109
+ write_failure "claude-smart currently supports Apple Silicon macOS 14+ only; Intel Mac is not supported because native ML wheels are unavailable."
110
+ fi
111
+ darwin_major="$(uname -r 2>/dev/null | awk -F. '{print $1}')"
112
+ case "$darwin_major" in
113
+ ''|*[!0-9]*)
114
+ write_failure "claude-smart could not determine the macOS version; Apple Silicon macOS 14+ is required."
115
+ ;;
116
+ esac
117
+ if [ "$darwin_major" -lt 23 ]; then
118
+ write_failure "claude-smart currently supports macOS 14+ on Apple Silicon; macOS 13 and older are not supported because native ML wheels are unavailable."
119
+ fi
120
+ ;;
121
+ MINGW*|MSYS*|CYGWIN*)
122
+ case "$machine" in
123
+ x86_64|amd64) : ;;
124
+ *)
125
+ write_failure "claude-smart currently supports Windows x64 only; Windows ARM is not supported because native ML wheels are unavailable."
126
+ ;;
127
+ esac
128
+ ;;
129
+ Linux*)
130
+ : # Existing Linux installs remain supported when package wheels are available.
131
+ ;;
132
+ *)
133
+ write_failure "claude-smart currently supports Apple Silicon macOS 14+, Windows x64, and Linux for vanilla installs."
134
+ ;;
135
+ esac
136
+ }
137
+
35
138
  install_private_node() {
36
139
  local NODE_MIN_MAJOR NODE_MIN_MINOR NODE_LTS_MAJOR
37
140
  local node_os archive_ext reason node_arch node_platform base_url node_root
@@ -236,6 +339,13 @@ if [ "${CLAUDE_SMART_INSTALL_PRIVATE_NODE_ONLY:-}" = "1" ]; then
236
339
  exit $?
237
340
  fi
238
341
 
342
+ preflight_supported_runtime_platform
343
+
344
+ if install_complete; then
345
+ echo '{"continue":true,"suppressOutput":true}'
346
+ exit 0
347
+ fi
348
+
239
349
  # Dev-mode only: when running from a git checkout, pull the reflexio
240
350
  # submodule so tests/benchmarks can use its sources. In install mode the
241
351
  # plugin lives under ~/.claude/plugins/cache and reflexio-ai resolves
@@ -327,57 +437,32 @@ if ! command -v claude >/dev/null 2>&1; then
327
437
  echo "[claude-smart] WARNING: 'claude' CLI not on PATH — reflexio extractors will have no LLM until it's installed" >&2
328
438
  fi
329
439
 
330
- # Allowlist cs-cite globally so Claude's citation Bash calls don't pop a
331
- # permission prompt mid-turn. Idempotent: no-ops when the entry is already
332
- # present. Uses Python to preserve the rest of settings.json intact.
333
- # Resolves python via claude_smart_resolve_python so we don't fire the
334
- # Windows App Execution Alias stub (which exits non-zero with "Python
335
- # was not found" when no real interpreter is installed).
440
+ LEGACY_CS_CITE="$HOME/.claude-smart/bin/cs-cite"
441
+ if [ -e "$LEGACY_CS_CITE" ]; then
442
+ rm -f "$LEGACY_CS_CITE"
443
+ echo "[claude-smart] removed legacy cs-cite helper at $LEGACY_CS_CITE" >&2
444
+ fi
445
+
336
446
  CLAUDE_SETTINGS="$HOME/.claude/settings.json"
337
- mkdir -p "$(dirname "$CLAUDE_SETTINGS")"
338
- PY_BIN=$(claude_smart_resolve_python || true)
339
- if [ -z "$PY_BIN" ]; then
340
- echo "[claude-smart] WARNING: no working python interpreter found; skipping cs-cite allowlist" >&2
341
- elif "$PY_BIN" - "$CLAUDE_SETTINGS" <<'PY' >&2
342
- import json
343
- import sys
344
- from pathlib import Path
345
-
346
- path = Path(sys.argv[1])
347
- entry = "Bash(cs-cite:*)"
348
- data: dict = {}
349
- if path.is_file():
350
- try:
351
- data = json.loads(path.read_text() or "{}")
352
- except json.JSONDecodeError:
353
- print(
354
- f"[claude-smart] WARNING: {path} is not valid JSON; skipping cs-cite allowlist",
355
- file=sys.stderr,
356
- )
357
- sys.exit(2)
358
- def _warn_and_exit(reason: str) -> None:
359
- print(
360
- f"[claude-smart] WARNING: {path} {reason}; skipping cs-cite allowlist",
361
- file=sys.stderr,
362
- )
363
- sys.exit(2)
364
-
365
- if not isinstance(data, dict):
366
- _warn_and_exit("top-level is not a JSON object")
367
- permissions = data.setdefault("permissions", {})
368
- if not isinstance(permissions, dict):
369
- _warn_and_exit("'permissions' is not a JSON object")
370
- allow = permissions.setdefault("allow", [])
371
- if not isinstance(allow, list):
372
- _warn_and_exit("'permissions.allow' is not a JSON array")
373
- if entry in allow:
374
- sys.exit(1) # already present — convey via exit code so shell can skip the log
375
- allow.append(entry)
376
- path.write_text(json.dumps(data, indent=2) + "\n")
377
- sys.exit(0)
378
- PY
379
- then
380
- echo "[claude-smart] added Bash(cs-cite:*) to $CLAUDE_SETTINGS permissions.allow" >&2
447
+ if [ -f "$CLAUDE_SETTINGS" ] && command -v node >/dev/null 2>&1; then
448
+ node - "$CLAUDE_SETTINGS" <<'JS' >&2 || true
449
+ const fs = require("fs");
450
+ const path = process.argv[2];
451
+ const entry = "Bash(cs-cite:*)";
452
+ let data;
453
+ try {
454
+ data = JSON.parse(fs.readFileSync(path, "utf8") || "{}");
455
+ } catch {
456
+ process.exit(0);
457
+ }
458
+ const allow = data?.permissions?.allow;
459
+ if (!Array.isArray(allow)) process.exit(0);
460
+ const next = allow.filter((item) => item !== entry);
461
+ if (next.length === allow.length) process.exit(0);
462
+ data.permissions.allow = next;
463
+ fs.writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
464
+ console.error(`[claude-smart] removed legacy ${entry} permission from ${path}`);
465
+ JS
381
466
  fi
382
467
 
383
468
  # Spawn the dashboard build detached so install returns immediately and
@@ -406,5 +491,6 @@ if ! bash "$HERE/ensure-plugin-root.sh" "$PLUGIN_ROOT"; then
406
491
  echo "[claude-smart] WARNING: failed to set ~/.reflexio/plugin-root symlink — slash commands may not resolve" >&2
407
492
  fi
408
493
 
494
+ write_success_marker
409
495
  echo "[claude-smart] install complete. Backend and dashboard auto-start on session start." >&2
410
496
  echo '{"continue":true,"suppressOutput":true}'