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.
- package/README.md +22 -1
- package/bin/claude-smart.js +333 -73
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +4 -0
- package/plugin/hooks/codex-hooks.json +5 -0
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +38 -0
- package/plugin/scripts/backend-log-runner.sh +33 -0
- package/plugin/scripts/backend-service.sh +15 -11
- package/plugin/scripts/cli.sh +27 -3
- package/plugin/scripts/codex-claude-compat +9 -0
- package/plugin/scripts/codex-claude-compat.cmd +4 -0
- package/plugin/scripts/codex-claude-compat.js +162 -0
- package/plugin/scripts/codex-hook.js +30 -2
- package/plugin/scripts/smart-install.sh +136 -50
- package/plugin/src/claude_smart/cli.py +101 -2
- package/plugin/src/claude_smart/context_inject.py +2 -4
- package/plugin/src/claude_smart/cs_cite.py +2 -90
- package/plugin/src/claude_smart/events/stop.py +16 -42
- package/plugin/src/claude_smart/internal_call.py +23 -0
- package/plugin/src/claude_smart/state.py +3 -3
- package/plugin/uv.lock +73 -76
- package/plugin/bin/cs-cite +0 -77
- package/plugin/scripts/codex-claude-compat.py +0 -144
package/plugin/hooks/hooks.json
CHANGED
|
@@ -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": [
|
package/plugin/pyproject.toml
CHANGED
package/plugin/scripts/_lib.sh
CHANGED
|
@@ -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
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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;
|
package/plugin/scripts/cli.sh
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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}'
|