claude-smart 0.2.28 → 0.2.30
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 +23 -16
- 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 +8 -3
- package/plugin/hooks/hooks.json +13 -3
- 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 +24 -12
- 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 +102 -11
- package/plugin/scripts/dashboard-service.sh +13 -4
- package/plugin/scripts/hook_entry.sh +29 -3
- package/plugin/scripts/smart-install.sh +176 -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/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,12 +182,20 @@ 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
|
-
|
|
186
|
-
|
|
189
|
+
if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
|
|
190
|
+
claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; running installer"
|
|
191
|
+
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true
|
|
192
|
+
claude_smart_source_login_path
|
|
193
|
+
claude_smart_prepend_astral_bins
|
|
194
|
+
fi
|
|
195
|
+
if ! command -v uv >/dev/null 2>&1; then
|
|
196
|
+
claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH after installer; skipping"
|
|
197
|
+
emit_ok; exit 0
|
|
198
|
+
fi
|
|
187
199
|
fi
|
|
188
200
|
cd "$PLUGIN_ROOT"
|
|
189
201
|
|
|
@@ -198,12 +210,12 @@ case "$CMD" in
|
|
|
198
210
|
# bookkeeping harder and we don't need hot-reload for a user-facing
|
|
199
211
|
# service. Detach via claude_smart_spawn_detached so the same code
|
|
200
212
|
# path covers Linux (setsid), macOS (python3 os.setsid), and Windows
|
|
201
|
-
# (nohup; no process groups).
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
213
|
+
# (nohup; no process groups). backend-log-runner.sh owns stdout/stderr
|
|
214
|
+
# capture so process output cannot grow backend.log past its cap.
|
|
215
|
+
claude_smart_spawn_detached bash "$HERE/backend-log-runner.sh" \
|
|
216
|
+
"$LOG_FILE" "$LOG_MAX_BYTES" -- \
|
|
217
|
+
uv run --project "$PLUGIN_ROOT" --quiet \
|
|
218
|
+
reflexio services start --only backend --no-reload
|
|
207
219
|
svc_pid=$!
|
|
208
220
|
# Record the spawned pid, not a pgid sampled with ps. On POSIX,
|
|
209
221
|
# 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() {
|
|
@@ -81,6 +104,10 @@ function npmPath() {
|
|
|
81
104
|
return commandPath(process.platform === "win32" ? ["npm.cmd", "npm.exe", "npm"] : ["npm"]);
|
|
82
105
|
}
|
|
83
106
|
|
|
107
|
+
function bashPath() {
|
|
108
|
+
return commandPath(process.platform === "win32" ? ["bash.exe", "bash"] : ["bash"]);
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
function stateFile(name) {
|
|
85
112
|
return path.join(STATE_DIR, name);
|
|
86
113
|
}
|
|
@@ -95,7 +122,10 @@ function writeBackendUrl(port) {
|
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
function codexCompatPath(root) {
|
|
98
|
-
|
|
125
|
+
const filename = process.platform === "win32"
|
|
126
|
+
? "codex-claude-compat.cmd"
|
|
127
|
+
: "codex-claude-compat";
|
|
128
|
+
return path.join(root, "scripts", filename);
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
function readBackendUrl() {
|
|
@@ -176,6 +206,49 @@ function detached(command, args, options = {}) {
|
|
|
176
206
|
return child.pid;
|
|
177
207
|
}
|
|
178
208
|
|
|
209
|
+
function runInstaller(root, reason) {
|
|
210
|
+
if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false;
|
|
211
|
+
const script = path.join(root, "scripts", "smart-install.sh");
|
|
212
|
+
if (!fs.existsSync(script)) return false;
|
|
213
|
+
const bash = bashPath();
|
|
214
|
+
if (!bash) return false;
|
|
215
|
+
appendLog("backend.log", `[claude-smart] ${reason}; running installer`);
|
|
216
|
+
const result = spawnSync(bash, [script], {
|
|
217
|
+
cwd: root,
|
|
218
|
+
env: {
|
|
219
|
+
...process.env,
|
|
220
|
+
CLAUDE_SMART_BOOTSTRAPPING: "1",
|
|
221
|
+
},
|
|
222
|
+
encoding: "utf8",
|
|
223
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
224
|
+
windowsHide: true,
|
|
225
|
+
});
|
|
226
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
227
|
+
if (output) {
|
|
228
|
+
ensureDir(STATE_DIR);
|
|
229
|
+
fs.appendFileSync(path.join(STATE_DIR, "install.log"), `${output}\n`);
|
|
230
|
+
trimLog(path.join(STATE_DIR, "install.log"));
|
|
231
|
+
}
|
|
232
|
+
prependRuntimePath();
|
|
233
|
+
return result.status === 0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function startInstallerDetached(root, reason) {
|
|
237
|
+
if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false;
|
|
238
|
+
const script = path.join(root, "scripts", "smart-install.sh");
|
|
239
|
+
const bash = bashPath();
|
|
240
|
+
if (!bash || !fs.existsSync(script)) return false;
|
|
241
|
+
appendLog("backend.log", `[claude-smart] ${reason}; starting installer in background`);
|
|
242
|
+
detached(bash, [script], {
|
|
243
|
+
cwd: root,
|
|
244
|
+
env: {
|
|
245
|
+
...process.env,
|
|
246
|
+
CLAUDE_SMART_BOOTSTRAPPING: "1",
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
179
252
|
function readPid(file) {
|
|
180
253
|
try {
|
|
181
254
|
const value = fs.readFileSync(file, "utf8").trim();
|
|
@@ -216,6 +289,7 @@ function ensurePluginRoot(root) {
|
|
|
216
289
|
}
|
|
217
290
|
|
|
218
291
|
async function startBackend(root) {
|
|
292
|
+
trimLog(path.join(STATE_DIR, "backend.log"));
|
|
219
293
|
if (process.env.CLAUDE_SMART_BACKEND_AUTOSTART === "0") {
|
|
220
294
|
emitOk();
|
|
221
295
|
return;
|
|
@@ -235,7 +309,11 @@ async function startBackend(root) {
|
|
|
235
309
|
}
|
|
236
310
|
const uv = uvPath();
|
|
237
311
|
if (!uv) {
|
|
238
|
-
|
|
312
|
+
runInstaller(root, "backend: uv not on PATH");
|
|
313
|
+
}
|
|
314
|
+
const readyUv = uvPath();
|
|
315
|
+
if (!readyUv) {
|
|
316
|
+
appendLog("backend.log", "[claude-smart] backend: uv not on PATH after installer; skipping");
|
|
239
317
|
emitOk();
|
|
240
318
|
return;
|
|
241
319
|
}
|
|
@@ -257,7 +335,7 @@ async function startBackend(root) {
|
|
|
257
335
|
INTERACTION_CLEANUP_DELETE_COUNT: process.env.INTERACTION_CLEANUP_DELETE_COUNT || "200",
|
|
258
336
|
};
|
|
259
337
|
const pid = detached(
|
|
260
|
-
|
|
338
|
+
readyUv,
|
|
261
339
|
[
|
|
262
340
|
"run",
|
|
263
341
|
"--project",
|
|
@@ -299,14 +377,18 @@ async function startDashboard(root) {
|
|
|
299
377
|
}
|
|
300
378
|
const npm = npmPath();
|
|
301
379
|
if (!npm) {
|
|
302
|
-
|
|
380
|
+
runInstaller(root, "dashboard: npm not on PATH");
|
|
381
|
+
}
|
|
382
|
+
const readyNpm = npmPath();
|
|
383
|
+
if (!readyNpm) {
|
|
384
|
+
appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH after installer; skipping");
|
|
303
385
|
emitOk();
|
|
304
386
|
return;
|
|
305
387
|
}
|
|
306
388
|
if (!fs.existsSync(path.join(dashboard, ".next"))) {
|
|
307
389
|
const buildPidFile = path.join(STATE_DIR, "dashboard-build.pid");
|
|
308
390
|
if (!pidAlive(readPid(buildPidFile))) {
|
|
309
|
-
const pid = detached(
|
|
391
|
+
const pid = detached(readyNpm, ["run", "build"], { cwd: dashboard });
|
|
310
392
|
writePid(buildPidFile, pid);
|
|
311
393
|
appendLog("dashboard.log", "[claude-smart] dashboard: .next missing; started background build");
|
|
312
394
|
}
|
|
@@ -319,18 +401,27 @@ async function startDashboard(root) {
|
|
|
319
401
|
REFLEXIO_URL: readBackendUrl(),
|
|
320
402
|
CLAUDE_SMART_DASHBOARD_WORKSPACE: process.cwd(),
|
|
321
403
|
};
|
|
322
|
-
const pid = detached(
|
|
404
|
+
const pid = detached(readyNpm, ["run", "start"], { cwd: dashboard, env });
|
|
323
405
|
writePid(pidFile, pid);
|
|
324
406
|
await waitForHealth(DASHBOARD_PORT, "/api/health", "x-claude-smart-dashboard", 5);
|
|
325
407
|
emitOk();
|
|
326
408
|
}
|
|
327
409
|
|
|
328
410
|
function runHook(root, event) {
|
|
329
|
-
|
|
411
|
+
trimLog(path.join(STATE_DIR, "backend.log"));
|
|
412
|
+
let uv = uvPath();
|
|
330
413
|
if (!uv) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
414
|
+
if (event === "session-start") {
|
|
415
|
+
runInstaller(root, "hook: uv not on PATH");
|
|
416
|
+
uv = uvPath();
|
|
417
|
+
} else {
|
|
418
|
+
startInstallerDetached(root, "hook: uv not on PATH");
|
|
419
|
+
}
|
|
420
|
+
if (!uv) {
|
|
421
|
+
appendLog("backend.log", "[claude-smart] hook: uv not on PATH after installer; skipping");
|
|
422
|
+
emitOk();
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
334
425
|
}
|
|
335
426
|
const input = fs.readFileSync(0);
|
|
336
427
|
const result = spawnSync(
|
|
@@ -100,10 +100,19 @@ case "$CMD" in
|
|
|
100
100
|
fi
|
|
101
101
|
NPM_BIN=$(claude_smart_resolve_npm || true)
|
|
102
102
|
if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
|
|
104
|
+
echo "[claude-smart] dashboard: npm is not on PATH; running installer" >>"$LOG_FILE"
|
|
105
|
+
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true
|
|
106
|
+
claude_smart_source_login_path
|
|
107
|
+
claude_smart_prepend_node_bins
|
|
108
|
+
NPM_BIN=$(claude_smart_resolve_npm || true)
|
|
109
|
+
fi
|
|
110
|
+
if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
|
|
111
|
+
reason="npm is not on PATH after installer; dashboard cannot start"
|
|
112
|
+
echo "[claude-smart] dashboard: $reason; skipping" >>"$LOG_FILE"
|
|
113
|
+
claude_smart_write_dashboard_unavailable "$reason"
|
|
114
|
+
emit_ok; exit 0
|
|
115
|
+
fi
|
|
107
116
|
fi
|
|
108
117
|
|
|
109
118
|
# `npm run start` requires a prior `next build`. Do NOT build in the
|
|
@@ -37,6 +37,7 @@ claude_smart_prepend_astral_bins
|
|
|
37
37
|
PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
|
|
38
38
|
|
|
39
39
|
FAILURE_MARKER="$HOME/.claude-smart/install-failed"
|
|
40
|
+
STATE_DIR="$HOME/.claude-smart"
|
|
40
41
|
if [ -f "$FAILURE_MARKER" ]; then
|
|
41
42
|
if [ "$EVENT" = "session-start" ] && command -v python3 >/dev/null 2>&1; then
|
|
42
43
|
python3 - "$FAILURE_MARKER" <<'PY'
|
|
@@ -61,9 +62,34 @@ PY
|
|
|
61
62
|
fi
|
|
62
63
|
|
|
63
64
|
if ! command -v uv >/dev/null 2>&1; then
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
# Self-heal from skipped Setup/SessionStart bootstrap. SessionStart can
|
|
66
|
+
# afford to wait because it has the install budget; prompt/tool hooks start
|
|
67
|
+
# the same installer detached so normal work is not blocked by first-run
|
|
68
|
+
# dependency setup.
|
|
69
|
+
if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then
|
|
70
|
+
echo '{"continue":true,"suppressOutput":true}'
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
|
|
74
|
+
mkdir -p "$STATE_DIR"
|
|
75
|
+
if [ "$EVENT" = "session-start" ]; then
|
|
76
|
+
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2
|
|
77
|
+
claude_smart_prepend_astral_bins
|
|
78
|
+
claude_smart_prepend_node_bins
|
|
79
|
+
if command -v uv >/dev/null 2>&1; then
|
|
80
|
+
bash "$HERE/backend-service.sh" start >/dev/null 2>&1 || true
|
|
81
|
+
bash "$HERE/dashboard-service.sh" start >/dev/null 2>&1 || true
|
|
82
|
+
fi
|
|
83
|
+
else
|
|
84
|
+
claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
|
|
85
|
+
bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
|
|
86
|
+
>>"$STATE_DIR/install.log" 2>&1 || true
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
if ! command -v uv >/dev/null 2>&1; then
|
|
90
|
+
echo '{"continue":true,"suppressOutput":true}'
|
|
91
|
+
exit 0
|
|
92
|
+
fi
|
|
67
93
|
fi
|
|
68
94
|
|
|
69
95
|
# Stdin is the hook payload JSON — stream it through to the Python CLI.
|