claude-smart 0.2.41 → 0.2.42
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/.claude-plugin/marketplace.json +17 -0
- package/README.md +1 -1
- package/bin/claude-smart.js +86 -48
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/next.config.ts +9 -1
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/backend-service.sh +45 -15
- package/plugin/scripts/cli.sh +28 -1
- package/plugin/scripts/ensure-plugin-root.sh +7 -14
- package/plugin/scripts/smart-install.sh +17 -2
- package/plugin/src/claude_smart/cli.py +72 -38
- package/plugin/uv.lock +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reflexioai",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "Yi Lu"
|
|
5
|
+
},
|
|
6
|
+
"metadata": {
|
|
7
|
+
"description": "claude-smart — self-improving Claude Code plugin via reflexio"
|
|
8
|
+
},
|
|
9
|
+
"plugins": [
|
|
10
|
+
{
|
|
11
|
+
"name": "claude-smart",
|
|
12
|
+
"version": "0.2.42",
|
|
13
|
+
"source": "./plugin",
|
|
14
|
+
"description": "Learns user corrections and project playbooks via reflexio — uses Claude Code itself as the LLM backend (no external API key required)"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License">
|
|
14
14
|
</a>
|
|
15
15
|
<a href="plugin/pyproject.toml">
|
|
16
|
-
<img src="https://img.shields.io/badge/version-0.2.
|
|
16
|
+
<img src="https://img.shields.io/badge/version-0.2.42-green.svg" alt="Version">
|
|
17
17
|
</a>
|
|
18
18
|
<a href="plugin/pyproject.toml">
|
|
19
19
|
<img src="https://img.shields.io/badge/python-%3E%3D3.12-brightgreen.svg" alt="Python">
|
package/bin/claude-smart.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* npx claude-smart install — thin wrapper around the native host plugin
|
|
4
|
-
* CLIs.
|
|
5
|
-
*
|
|
6
|
-
* and
|
|
7
|
-
*
|
|
8
|
-
* with no API key.
|
|
9
|
-
* `npx claude-smart setup`,
|
|
10
|
-
* installer.
|
|
4
|
+
* CLIs. Both Claude Code and Codex install from the bundled marketplace in
|
|
5
|
+
* this npm package: Claude Code registers the package root as a local
|
|
6
|
+
* marketplace, and Codex copies the bundled plugin into its own marketplace
|
|
7
|
+
* wrapper. Both paths seed ~/.reflexio/.env with the two local-provider flags
|
|
8
|
+
* so reflexio can route generation through local tools with no API key.
|
|
9
|
+
* Managed/read-only/global setup is handled by `npx claude-smart setup`,
|
|
10
|
+
* which writes ~/.reflexio/.env before running this installer.
|
|
11
11
|
*
|
|
12
12
|
* Keep this file dependency-free — it runs via `npx` with no install step.
|
|
13
13
|
*/
|
|
@@ -33,7 +33,6 @@ const https = require("https");
|
|
|
33
33
|
const { arch, homedir, platform, release, tmpdir } = require("os");
|
|
34
34
|
const { dirname, join } = require("path");
|
|
35
35
|
|
|
36
|
-
const DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart";
|
|
37
36
|
const PLUGIN_SPEC = "claude-smart@reflexioai";
|
|
38
37
|
const CODEX_MARKETPLACE_NAME = "reflexioai";
|
|
39
38
|
const CODEX_MARKETPLACE_DISPLAY_NAME = "ReflexioAI";
|
|
@@ -358,7 +357,7 @@ function findClaudeCodePluginRoot() {
|
|
|
358
357
|
}
|
|
359
358
|
}
|
|
360
359
|
} catch {
|
|
361
|
-
// Fall through to marketplace
|
|
360
|
+
// Fall through to marketplace fallback.
|
|
362
361
|
}
|
|
363
362
|
candidates.sort((a, b) => {
|
|
364
363
|
const versionCompare = compareSemverLikePathNames(b, a);
|
|
@@ -369,9 +368,13 @@ function findClaudeCodePluginRoot() {
|
|
|
369
368
|
return 0;
|
|
370
369
|
}
|
|
371
370
|
});
|
|
371
|
+
// PACKAGE_ROOT/plugin is intentionally NOT a fallback: under `npx` it
|
|
372
|
+
// points at /root/.npm/_npx/<hash>/.../plugin which npm may prune between
|
|
373
|
+
// invocations, breaking the editable claude_smart install (.pth dangles,
|
|
374
|
+
// venv survives) and confusing every later `uv sync`. Require `claude
|
|
375
|
+
// plugin install` to have populated the cache dir instead, and fail loud.
|
|
372
376
|
const fallbacks = [
|
|
373
377
|
join(homedir(), ".claude", "plugins", "marketplaces", CODEX_MARKETPLACE_NAME, "plugin"),
|
|
374
|
-
join(PACKAGE_ROOT, "plugin"),
|
|
375
378
|
];
|
|
376
379
|
for (const candidate of [...candidates, ...fallbacks]) {
|
|
377
380
|
if (
|
|
@@ -429,7 +432,19 @@ function forcePluginRoot(pluginRoot) {
|
|
|
429
432
|
async function bootstrapClaudeCodeInstall() {
|
|
430
433
|
const pluginRoot = findClaudeCodePluginRoot();
|
|
431
434
|
if (!pluginRoot) {
|
|
432
|
-
|
|
435
|
+
const cacheRoot = join(
|
|
436
|
+
homedir(),
|
|
437
|
+
".claude",
|
|
438
|
+
"plugins",
|
|
439
|
+
"cache",
|
|
440
|
+
CODEX_MARKETPLACE_NAME,
|
|
441
|
+
"claude-smart",
|
|
442
|
+
);
|
|
443
|
+
throw new Error(
|
|
444
|
+
`claude plugin install did not populate ${cacheRoot}. ` +
|
|
445
|
+
"Open Claude Code, run /plugins, verify claude-smart is installed " +
|
|
446
|
+
"from the ReflexioAI marketplace, then rerun `npx claude-smart install`.",
|
|
447
|
+
);
|
|
433
448
|
}
|
|
434
449
|
forcePluginRoot(pluginRoot);
|
|
435
450
|
const bash = resolveCommand(isWindows() ? ["bash.exe", "bash"] : ["bash"]);
|
|
@@ -960,13 +975,12 @@ function printHelp() {
|
|
|
960
975
|
"Usage:",
|
|
961
976
|
" npx claude-smart install Install the plugin into Claude Code",
|
|
962
977
|
" npx claude-smart install --host codex Register the plugin marketplace for Codex",
|
|
963
|
-
" npx claude-smart install --source <owner/repo> Override the marketplace source",
|
|
964
978
|
" npx claude-smart setup Configure managed/read-only/global setup",
|
|
965
979
|
" npx claude-smart uninstall --host codex Remove the Codex marketplace registration",
|
|
966
980
|
" npx claude-smart --help Show this help",
|
|
967
981
|
"",
|
|
968
982
|
"Claude Code install:",
|
|
969
|
-
" 1. claude plugin marketplace add <
|
|
983
|
+
" 1. claude plugin marketplace add <this package>",
|
|
970
984
|
` 2. claude plugin install ${PLUGIN_SPEC}`,
|
|
971
985
|
" 3. Reads ~/.reflexio/.env when managed/read-only setup was configured.",
|
|
972
986
|
"",
|
|
@@ -980,7 +994,8 @@ function printHelp() {
|
|
|
980
994
|
" 7. Restart Codex.",
|
|
981
995
|
"",
|
|
982
996
|
"Update:",
|
|
983
|
-
" npx claude-smart update
|
|
997
|
+
" npx claude-smart update Reinstall Claude Code support from this package",
|
|
998
|
+
" npx claude-smart update --host codex Reinstall Codex support from this package",
|
|
984
999
|
" npx claude-smart setup Configure managed/read-only/global setup",
|
|
985
1000
|
"",
|
|
986
1001
|
"Uninstall:",
|
|
@@ -990,17 +1005,6 @@ function printHelp() {
|
|
|
990
1005
|
);
|
|
991
1006
|
}
|
|
992
1007
|
|
|
993
|
-
function parseSource(args) {
|
|
994
|
-
const idx = args.indexOf("--source");
|
|
995
|
-
if (idx === -1) return DEFAULT_MARKETPLACE_SOURCE;
|
|
996
|
-
const value = args[idx + 1];
|
|
997
|
-
if (!value) {
|
|
998
|
-
process.stderr.write("error: --source requires a value (e.g. owner/repo)\n");
|
|
999
|
-
process.exit(1);
|
|
1000
|
-
}
|
|
1001
|
-
return value;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
1008
|
function parseHost(args) {
|
|
1005
1009
|
const idx = args.indexOf("--host");
|
|
1006
1010
|
if (idx === -1) return "claude-code";
|
|
@@ -1396,33 +1400,53 @@ function installCodexPluginCache(pluginRoot) {
|
|
|
1396
1400
|
return cacheDir;
|
|
1397
1401
|
}
|
|
1398
1402
|
|
|
1399
|
-
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1403
|
+
function findCodexPluginRoot() {
|
|
1404
|
+
const candidates = [];
|
|
1405
|
+
try {
|
|
1406
|
+
for (const entry of readdirSync(CODEX_PLUGIN_CACHE_DIR, { withFileTypes: true })) {
|
|
1407
|
+
if (!entry.isDirectory()) continue;
|
|
1408
|
+
const candidate = join(CODEX_PLUGIN_CACHE_DIR, entry.name);
|
|
1409
|
+
if (
|
|
1410
|
+
existsSync(join(candidate, "pyproject.toml")) &&
|
|
1411
|
+
existsSync(join(candidate, "scripts", "smart-install.sh"))
|
|
1412
|
+
) {
|
|
1413
|
+
candidates.push(candidate);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
} catch {
|
|
1417
|
+
// No Codex cache yet.
|
|
1407
1418
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1419
|
+
candidates.sort((a, b) => {
|
|
1420
|
+
const versionCompare = compareSemverLikePathNames(b, a);
|
|
1421
|
+
if (versionCompare !== 0) return versionCompare;
|
|
1422
|
+
try {
|
|
1423
|
+
return statSync(b).mtimeMs - statSync(a).mtimeMs;
|
|
1424
|
+
} catch {
|
|
1425
|
+
return 0;
|
|
1426
|
+
}
|
|
1411
1427
|
});
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1428
|
+
return candidates[0] || null;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async function runUpdate(args) {
|
|
1432
|
+
if (parseHost(args) === "codex") {
|
|
1433
|
+
await runUpdateCodex(args);
|
|
1434
|
+
return;
|
|
1415
1435
|
}
|
|
1416
1436
|
|
|
1417
|
-
process.stdout.write("\nclaude-smart updated. Restart Claude Code to apply.\n");
|
|
1418
1437
|
const pluginRoot = findClaudeCodePluginRoot();
|
|
1419
1438
|
if (pluginRoot) {
|
|
1420
|
-
|
|
1421
|
-
if (setup.readOnly) {
|
|
1422
|
-
prunePublishHooksForReadOnly(pluginRoot);
|
|
1423
|
-
process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
|
|
1424
|
-
}
|
|
1439
|
+
stopClaudeSmartServices(pluginRoot);
|
|
1425
1440
|
}
|
|
1441
|
+
process.stdout.write("Updating claude-smart by reinstalling from this package...\n");
|
|
1442
|
+
await runInstall(args, { retryInstallAfterUninstall: true });
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
async function runUpdateCodex(args) {
|
|
1446
|
+
const pluginRoot = findCodexPluginRoot() || join(PACKAGE_ROOT, "plugin");
|
|
1447
|
+
stopClaudeSmartServices(pluginRoot);
|
|
1448
|
+
process.stdout.write("Updating claude-smart Codex support by reinstalling from this package...\n");
|
|
1449
|
+
await runInstallCodex(args);
|
|
1426
1450
|
}
|
|
1427
1451
|
|
|
1428
1452
|
async function runUninstall(args) {
|
|
@@ -1475,7 +1499,7 @@ async function runSetup(args) {
|
|
|
1475
1499
|
if (code !== 0) process.exit(code);
|
|
1476
1500
|
}
|
|
1477
1501
|
|
|
1478
|
-
async function runInstall(args) {
|
|
1502
|
+
async function runInstall(args, options = {}) {
|
|
1479
1503
|
if (parseHost(args) === "codex") {
|
|
1480
1504
|
await runInstallCodex(args);
|
|
1481
1505
|
return;
|
|
@@ -1489,7 +1513,7 @@ async function runInstall(args) {
|
|
|
1489
1513
|
process.exit(1);
|
|
1490
1514
|
}
|
|
1491
1515
|
|
|
1492
|
-
const source =
|
|
1516
|
+
const source = PACKAGE_ROOT;
|
|
1493
1517
|
const setup = configureReflexioSetup();
|
|
1494
1518
|
const readOnly = setup.readOnly;
|
|
1495
1519
|
|
|
@@ -1499,7 +1523,21 @@ async function runInstall(args) {
|
|
|
1499
1523
|
];
|
|
1500
1524
|
|
|
1501
1525
|
for (const step of steps) {
|
|
1502
|
-
|
|
1526
|
+
let code = await runClaude(step.args, { spinnerLabel: step.label });
|
|
1527
|
+
if (
|
|
1528
|
+
code !== 0 &&
|
|
1529
|
+
options.retryInstallAfterUninstall &&
|
|
1530
|
+
step.args[0] === "plugin" &&
|
|
1531
|
+
step.args[1] === "install"
|
|
1532
|
+
) {
|
|
1533
|
+
process.stderr.write(
|
|
1534
|
+
`warning: \`claude ${step.args.join(" ")}\` failed (exit ${code}); retrying after uninstalling ${PLUGIN_SPEC}.\n`,
|
|
1535
|
+
);
|
|
1536
|
+
await runClaude(["plugin", "uninstall", PLUGIN_SPEC], {
|
|
1537
|
+
spinnerLabel: "Removing existing claude-smart install…",
|
|
1538
|
+
});
|
|
1539
|
+
code = await runClaude(step.args, { spinnerLabel: step.label });
|
|
1540
|
+
}
|
|
1503
1541
|
if (code !== 0) {
|
|
1504
1542
|
process.stderr.write(
|
|
1505
1543
|
`error: \`claude ${step.args.join(" ")}\` failed (exit ${code})\n`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-smart",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.42",
|
|
4
4
|
"description": "Self-improving Claude Code plugin — learns from corrections via reflexio",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"bin",
|
|
30
30
|
"scripts/setup-claude-smart.sh",
|
|
31
31
|
".agents/plugins/marketplace.json",
|
|
32
|
+
".claude-plugin/marketplace.json",
|
|
32
33
|
"plugin",
|
|
33
34
|
"!plugin/**/__pycache__",
|
|
34
35
|
"!plugin/**/*.py[cod]",
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
|
+
import path from "path";
|
|
2
3
|
|
|
4
|
+
// Pin Turbopack's workspace root to this dashboard directory. When the
|
|
5
|
+
// plugin is installed via npx (e.g. ~/.npm/_npx/<hash>/node_modules/...),
|
|
6
|
+
// Next would otherwise detect the npx parent's package-lock.json as the
|
|
7
|
+
// workspace root and panic in `next build` ("Failed to write app endpoint
|
|
8
|
+
// /page"). Pinning the root avoids that misdetection.
|
|
3
9
|
const nextConfig: NextConfig = {
|
|
4
|
-
|
|
10
|
+
turbopack: {
|
|
11
|
+
root: path.resolve(__dirname),
|
|
12
|
+
},
|
|
5
13
|
};
|
|
6
14
|
|
|
7
15
|
export default nextConfig;
|
package/plugin/pyproject.toml
CHANGED
|
@@ -138,20 +138,27 @@ port_occupied() {
|
|
|
138
138
|
(echo >"/dev/tcp/127.0.0.1/$PORT") 2>/dev/null
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
# Reap
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
-
#
|
|
141
|
+
# Reap listeners still holding the given port after the PID file kill
|
|
142
|
+
# when their command line matches one of the supplied patterns. Filters
|
|
143
|
+
# by cmdline so we don't knock over an
|
|
144
|
+
# unrelated service a user has bound there — symmetric with start's
|
|
145
|
+
# refusal to stomp on a foreign listener. Silent on failure.
|
|
145
146
|
reap_port_listeners() {
|
|
147
|
+
port="${1:-$PORT}"
|
|
148
|
+
shift || true
|
|
149
|
+
[ "$#" -eq 0 ] && return 0
|
|
146
150
|
command -v lsof >/dev/null 2>&1 || return 0
|
|
147
|
-
candidates=$(lsof -ti:"$
|
|
151
|
+
candidates=$(lsof -ti:"$port" 2>/dev/null) || candidates=""
|
|
148
152
|
[ -z "$candidates" ] && return 0
|
|
149
153
|
ours=""
|
|
150
154
|
for pid in $candidates; do
|
|
151
155
|
cmdline=$(ps -p "$pid" -o command= 2>/dev/null || true)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
for pattern in "$@"; do
|
|
157
|
+
if [[ "$cmdline" == $pattern ]]; then
|
|
158
|
+
ours="$ours $pid"
|
|
159
|
+
break
|
|
160
|
+
fi
|
|
161
|
+
done
|
|
155
162
|
done
|
|
156
163
|
[ -z "$ours" ] && return 0
|
|
157
164
|
# shellcheck disable=SC2086
|
|
@@ -166,16 +173,33 @@ reap_port_listeners() {
|
|
|
166
173
|
kill -KILL $remaining 2>/dev/null || true
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
|
|
176
|
+
# Describe what (if anything) is currently listening on $1. Returns
|
|
177
|
+
# "<command> (pid <pid>)" or empty if the port is free or lsof is
|
|
178
|
+
# unavailable. Used to make port-conflict log lines diagnosable.
|
|
179
|
+
port_holder() {
|
|
180
|
+
command -v lsof >/dev/null 2>&1 || return 0
|
|
181
|
+
lsof -i:"$1" -sTCP:LISTEN -P -n 2>/dev/null \
|
|
182
|
+
| awk 'NR==2 {print $1" (pid "$2")"; exit}'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Full shutdown: kill the recorded process group (if any) then sweep
|
|
186
|
+
# both the backend port and the embedding-service port for surviving
|
|
187
|
+
# reflexio listeners. Used by both `stop` and the opt-in `session-end`
|
|
188
|
+
# path so a stale/missing PID file doesn't produce a silent no-op, and
|
|
189
|
+
# so a stale embedding service on EMBEDDING_PORT doesn't block the next
|
|
190
|
+
# fresh boot (e.g. when codex-hook.js falls back to 8072 for the main
|
|
191
|
+
# backend because 8071 is held by another app).
|
|
173
192
|
full_stop() {
|
|
174
193
|
if [ -f "$PID_FILE" ]; then
|
|
175
194
|
kill_group "$(cat "$PID_FILE" 2>/dev/null)"
|
|
176
195
|
rm -f "$PID_FILE"
|
|
177
196
|
fi
|
|
178
|
-
reap_port_listeners
|
|
197
|
+
reap_port_listeners "$PORT" '*reflexio*' '*uvicorn*'
|
|
198
|
+
if [ -n "${EMBEDDING_PORT:-}" ] && [ "$EMBEDDING_PORT" != "$PORT" ]; then
|
|
199
|
+
reap_port_listeners "$EMBEDDING_PORT" \
|
|
200
|
+
'*reflexio.server.llm.embedding_service:app*' \
|
|
201
|
+
'*reflexio*embedding_service*'
|
|
202
|
+
fi
|
|
179
203
|
}
|
|
180
204
|
|
|
181
205
|
case "$CMD" in
|
|
@@ -195,8 +219,14 @@ case "$CMD" in
|
|
|
195
219
|
if is_our_backend_running; then emit_ok; exit 0; fi
|
|
196
220
|
if port_occupied; then
|
|
197
221
|
# Something answered the TCP probe but /health didn't — don't
|
|
198
|
-
# start a second uvicorn on top of it.
|
|
199
|
-
|
|
222
|
+
# start a second uvicorn on top of it. Surface the holder so the
|
|
223
|
+
# user knows which process to quit (common case: editor/dev tool
|
|
224
|
+
# squatting 8071) instead of silently failing.
|
|
225
|
+
holder="$(port_holder "$PORT" 2>/dev/null || true)"
|
|
226
|
+
msg="[claude-smart] backend: port $PORT held by another process${holder:+ ($holder)}; skipping start"
|
|
227
|
+
claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "$msg"
|
|
228
|
+
echo "$msg" >&2
|
|
229
|
+
echo "Free port $PORT (or stop the process above) and run /claude-smart:restart again." >&2
|
|
200
230
|
emit_ok; exit 0
|
|
201
231
|
fi
|
|
202
232
|
if ! command -v uv >/dev/null 2>&1; then
|
package/plugin/scripts/cli.sh
CHANGED
|
@@ -45,7 +45,7 @@ if ! command -v uv >/dev/null 2>&1; then
|
|
|
45
45
|
fi
|
|
46
46
|
if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
|
|
47
47
|
echo "claude-smart: 'uv' not found — bootstrapping dependencies (~1-3 min on first install)..." >&2
|
|
48
|
-
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh"
|
|
48
|
+
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >/dev/null
|
|
49
49
|
claude_smart_prepend_astral_bins
|
|
50
50
|
claude_smart_prepend_node_bins
|
|
51
51
|
fi
|
|
@@ -63,4 +63,31 @@ if ! command -v uv >/dev/null 2>&1; then
|
|
|
63
63
|
fi
|
|
64
64
|
fi
|
|
65
65
|
|
|
66
|
+
# Slash commands run synchronously and surface stderr directly to the user,
|
|
67
|
+
# so a bare `python -m claude_smart.cli` ModuleNotFoundError leaks to the
|
|
68
|
+
# transcript when the install is mid-bootstrap (e.g. SessionStart's
|
|
69
|
+
# background smart-install.sh hasn't finished yet). Mirror hook_entry.sh's
|
|
70
|
+
# ensure_hook_package_importable check: run smart-install.sh inline once,
|
|
71
|
+
# then re-check, then surface either the install-failed marker reason or a
|
|
72
|
+
# clear "still bootstrapping" message instead of letting Python crash.
|
|
73
|
+
if ! claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.cli; then
|
|
74
|
+
if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] \
|
|
75
|
+
&& [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
|
|
76
|
+
echo "claude-smart: finishing install (~1-3 min on first install)..." >&2
|
|
77
|
+
CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >/dev/null || true
|
|
78
|
+
fi
|
|
79
|
+
if ! claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.cli; then
|
|
80
|
+
if [ -f "$FAILURE_MARKER" ]; then
|
|
81
|
+
msg="$(head -n 1 "$FAILURE_MARKER" 2>/dev/null || echo "")"
|
|
82
|
+
[ -n "$msg" ] || msg="unknown error"
|
|
83
|
+
echo "claude-smart is not installed correctly: $msg" >&2
|
|
84
|
+
echo "Re-run the plugin's Setup (restart Claude Code) or fix the underlying issue and delete $FAILURE_MARKER to retry." >&2
|
|
85
|
+
else
|
|
86
|
+
echo "claude-smart: claude_smart package is not importable in $PLUGIN_ROOT/.venv." >&2
|
|
87
|
+
echo "Run $PLUGIN_ROOT/scripts/smart-install.sh manually to diagnose." >&2
|
|
88
|
+
fi
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
66
93
|
exec uv run --project "$PLUGIN_ROOT" --no-sync --quiet python -m claude_smart.cli "$@"
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Maintain ~/.reflexio/plugin-root as a symlink to the active plugin
|
|
3
|
-
# install dir so slash commands can reference one short path
|
|
4
|
-
# of whether the user is on the remote marketplace (reflexioai) or the
|
|
5
|
-
# local-dev marketplace (reflexioai-local).
|
|
3
|
+
# install dir so slash commands can reference one short path.
|
|
6
4
|
#
|
|
7
5
|
# Usage: ensure-plugin-root.sh <target-dir> [--force]
|
|
8
|
-
# --force overwrite any existing link
|
|
6
|
+
# --force overwrite any existing link
|
|
9
7
|
# default self-heal only if the link is missing or points at an
|
|
10
8
|
# invalid target
|
|
11
9
|
set -eu
|
|
@@ -34,8 +32,7 @@ fi
|
|
|
34
32
|
|
|
35
33
|
# Opt-in: when CLAUDE_SMART_PLUGIN_ROOT_FOLLOW_SESSION=1 (set in the
|
|
36
34
|
# environment or in ~/.reflexio/.env), always relink to $TARGET so the
|
|
37
|
-
# symlink tracks the currently loaded plugin.
|
|
38
|
-
# a pinned local-dev link across sessions that load the remote plugin.
|
|
35
|
+
# symlink tracks the currently loaded plugin.
|
|
39
36
|
FOLLOW="${CLAUDE_SMART_PLUGIN_ROOT_FOLLOW_SESSION:-}"
|
|
40
37
|
if [ -z "$FOLLOW" ] && [ -f "$HOME/.reflexio/.env" ]; then
|
|
41
38
|
FOLLOW="$(grep -E '^CLAUDE_SMART_PLUGIN_ROOT_FOLLOW_SESSION=' "$HOME/.reflexio/.env" \
|
|
@@ -52,11 +49,9 @@ fi
|
|
|
52
49
|
|
|
53
50
|
# Cache-tracking: if the link currently resolves to a path under the
|
|
54
51
|
# managed plugin cache (~/.claude/plugins/cache/ or ~/.codex/plugins/cache/),
|
|
55
|
-
# always retarget it to
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# fresh. Links pointing outside the cache (e.g., a user's local-dev
|
|
59
|
-
# checkout) are left alone here and handled by the self-heal below.
|
|
52
|
+
# always retarget it to $TARGET. Plugin updates leave old version
|
|
53
|
+
# directories behind, so a valid pyproject.toml at the stale target is
|
|
54
|
+
# not proof the link is fresh.
|
|
60
55
|
if [ -L "$LINK" ]; then
|
|
61
56
|
# Literal target string, not realpath: we compare against what was written by ln -s.
|
|
62
57
|
CURRENT="$(readlink "$LINK" 2>/dev/null || true)"
|
|
@@ -74,9 +69,7 @@ if [ -L "$LINK" ]; then
|
|
|
74
69
|
fi
|
|
75
70
|
|
|
76
71
|
# Self-heal path: only rewrite the link if it's missing or its target is
|
|
77
|
-
# gone/invalid.
|
|
78
|
-
# setup-local-dev.sh, so SessionStart hooks on the local install don't
|
|
79
|
-
# clobber the user's repo-pointing link.
|
|
72
|
+
# gone/invalid.
|
|
80
73
|
if [ -f "$LINK/pyproject.toml" ]; then
|
|
81
74
|
exit 0
|
|
82
75
|
fi
|
|
@@ -446,6 +446,22 @@ if ! command -v uv >/dev/null 2>&1; then
|
|
|
446
446
|
fi
|
|
447
447
|
|
|
448
448
|
cd "$PLUGIN_ROOT"
|
|
449
|
+
# Self-heal a corrupt .venv before uv sync. uv refuses to reuse a venv that
|
|
450
|
+
# exists but has no python executable (e.g. partial cleanup of an npm/npx
|
|
451
|
+
# tree, an interrupted earlier install, or the underlying interpreter being
|
|
452
|
+
# uninstalled) — it errors with "not a valid Python environment" instead of
|
|
453
|
+
# rebuilding. Pre-clearing lets uv recreate it cleanly.
|
|
454
|
+
if [ -d "$PLUGIN_ROOT/.venv" ]; then
|
|
455
|
+
if claude_smart_is_windows; then
|
|
456
|
+
plugin_python_path="$PLUGIN_ROOT/.venv/Scripts/python.exe"
|
|
457
|
+
else
|
|
458
|
+
plugin_python_path="$PLUGIN_ROOT/.venv/bin/python"
|
|
459
|
+
fi
|
|
460
|
+
if [ ! -x "$plugin_python_path" ]; then
|
|
461
|
+
echo "[claude-smart] .venv at $PLUGIN_ROOT/.venv has no python executable; recreating" >&2
|
|
462
|
+
rm -rf "$PLUGIN_ROOT/.venv"
|
|
463
|
+
fi
|
|
464
|
+
fi
|
|
449
465
|
echo "[claude-smart] running uv sync..." >&2
|
|
450
466
|
if ! uv sync --locked --python 3.12 --quiet >&2; then
|
|
451
467
|
write_failure "uv sync failed in $PLUGIN_ROOT — run 'uv sync --locked --python 3.12' there to diagnose"
|
|
@@ -626,8 +642,7 @@ elif [ -d "$DASHBOARD_DIR" ]; then
|
|
|
626
642
|
fi
|
|
627
643
|
|
|
628
644
|
# Point ~/.reflexio/plugin-root at this install so slash commands can
|
|
629
|
-
# reference one stable short path regardless of which
|
|
630
|
-
# (reflexioai or reflexioai-local) loaded us.
|
|
645
|
+
# reference one stable short path regardless of which install loaded us.
|
|
631
646
|
if ! bash "$HERE/ensure-plugin-root.sh" "$PLUGIN_ROOT"; then
|
|
632
647
|
echo "[claude-smart] WARNING: failed to set ~/.reflexio/plugin-root symlink — slash commands may not resolve" >&2
|
|
633
648
|
fi
|
|
@@ -38,7 +38,6 @@ from claude_smart.reflexio_adapter import Adapter
|
|
|
38
38
|
|
|
39
39
|
_REFLEXIO_ENV_PATH = env_config.REFLEXIO_ENV_PATH
|
|
40
40
|
_MANAGED_REFLEXIO_URL = env_config.MANAGED_REFLEXIO_URL
|
|
41
|
-
_DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart"
|
|
42
41
|
_PLUGIN_SPEC = "claude-smart@reflexioai"
|
|
43
42
|
_CODEX_MARKETPLACE_NAME = "reflexioai"
|
|
44
43
|
_CODEX_MARKETPLACE_DISPLAY_NAME = "ReflexioAI"
|
|
@@ -803,6 +802,23 @@ def _install_codex_plugin_cache(plugin_root: Path) -> tuple[bool, str]:
|
|
|
803
802
|
return True, f"installed Codex plugin cache at {cache_dir}"
|
|
804
803
|
|
|
805
804
|
|
|
805
|
+
def _find_codex_plugin_root() -> Path | None:
|
|
806
|
+
"""Locate the latest installed Codex plugin cache root."""
|
|
807
|
+
candidates = (
|
|
808
|
+
[
|
|
809
|
+
child
|
|
810
|
+
for child in _CODEX_PLUGIN_CACHE_DIR.iterdir()
|
|
811
|
+
if child.is_dir()
|
|
812
|
+
and (child / "pyproject.toml").is_file()
|
|
813
|
+
and (child / "scripts" / "smart-install.sh").is_file()
|
|
814
|
+
]
|
|
815
|
+
if _CODEX_PLUGIN_CACHE_DIR.is_dir()
|
|
816
|
+
else []
|
|
817
|
+
)
|
|
818
|
+
candidates.sort(key=_installed_plugin_sort_key, reverse=True)
|
|
819
|
+
return candidates[0] if candidates else None
|
|
820
|
+
|
|
821
|
+
|
|
806
822
|
def _cleanup_codex_install_state() -> bool:
|
|
807
823
|
"""Remove Codex's local install artifacts while preserving shared learning data.
|
|
808
824
|
|
|
@@ -1055,13 +1071,13 @@ def cmd_install_codex(args: argparse.Namespace) -> int:
|
|
|
1055
1071
|
def cmd_install(args: argparse.Namespace) -> int:
|
|
1056
1072
|
"""Install claude-smart into Claude Code via the native plugin CLI.
|
|
1057
1073
|
|
|
1058
|
-
Runs ``claude plugin marketplace add``
|
|
1059
|
-
|
|
1060
|
-
|
|
1074
|
+
Runs ``claude plugin marketplace add`` (pointed at the bundled marketplace
|
|
1075
|
+
at ``_REPO_ROOT``) followed by ``claude plugin install``, then appends the
|
|
1076
|
+
local-provider flags to ``~/.reflexio/.env`` so reflexio can route
|
|
1077
|
+
generation through the local Claude Code CLI.
|
|
1061
1078
|
|
|
1062
1079
|
Args:
|
|
1063
|
-
args (argparse.Namespace): Parsed CLI args.
|
|
1064
|
-
marketplace ref (``owner/repo`` on GitHub, or a local directory).
|
|
1080
|
+
args (argparse.Namespace): Parsed CLI args.
|
|
1065
1081
|
|
|
1066
1082
|
Returns:
|
|
1067
1083
|
int: 0 on success, non-zero if the ``claude`` CLI is missing or fails.
|
|
@@ -1077,13 +1093,27 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
1077
1093
|
return 1
|
|
1078
1094
|
read_only = _configure_reflexio_setup()
|
|
1079
1095
|
|
|
1096
|
+
refresh_existing = bool(getattr(args, "refresh_existing", False))
|
|
1080
1097
|
for cmd in (
|
|
1081
|
-
["claude", "plugin", "marketplace", "add",
|
|
1098
|
+
["claude", "plugin", "marketplace", "add", str(_REPO_ROOT)],
|
|
1082
1099
|
["claude", "plugin", "install", _PLUGIN_SPEC],
|
|
1083
1100
|
):
|
|
1084
1101
|
try:
|
|
1085
1102
|
subprocess.run(cmd, check=True)
|
|
1086
1103
|
except subprocess.CalledProcessError as exc:
|
|
1104
|
+
if refresh_existing and cmd[1:3] == ["plugin", "install"]:
|
|
1105
|
+
sys.stderr.write(
|
|
1106
|
+
f"warning: {' '.join(cmd)} failed (exit {exc.returncode}); "
|
|
1107
|
+
f"retrying after uninstalling {_PLUGIN_SPEC}\n"
|
|
1108
|
+
)
|
|
1109
|
+
subprocess.run(
|
|
1110
|
+
["claude", "plugin", "uninstall", _PLUGIN_SPEC], check=False
|
|
1111
|
+
)
|
|
1112
|
+
try:
|
|
1113
|
+
subprocess.run(cmd, check=True)
|
|
1114
|
+
continue
|
|
1115
|
+
except subprocess.CalledProcessError as retry_exc:
|
|
1116
|
+
exc = retry_exc
|
|
1087
1117
|
sys.stderr.write(f"error: {' '.join(cmd)} failed (exit {exc.returncode})\n")
|
|
1088
1118
|
return exc.returncode or 1
|
|
1089
1119
|
|
|
@@ -1112,41 +1142,44 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
1112
1142
|
|
|
1113
1143
|
|
|
1114
1144
|
def cmd_update(args: argparse.Namespace) -> int:
|
|
1115
|
-
"""Update claude-smart
|
|
1145
|
+
"""Update claude-smart by reinstalling from this package.
|
|
1116
1146
|
|
|
1117
|
-
|
|
1147
|
+
The simplified install path registers the bundled package root as the
|
|
1148
|
+
marketplace, so update is a stop + reinstall instead of
|
|
1149
|
+
``claude plugin update``.
|
|
1118
1150
|
|
|
1119
1151
|
Args:
|
|
1120
|
-
args (argparse.Namespace): Parsed CLI args
|
|
1152
|
+
args (argparse.Namespace): Parsed CLI args.
|
|
1121
1153
|
|
|
1122
1154
|
Returns:
|
|
1123
|
-
int: 0 on success, non-zero if the
|
|
1155
|
+
int: 0 on success, non-zero if the host CLI is missing or install fails.
|
|
1124
1156
|
"""
|
|
1125
|
-
if
|
|
1126
|
-
|
|
1127
|
-
"error: 'claude' CLI not found on PATH. "
|
|
1128
|
-
"Install Claude Code first: https://claude.com/claude-code\n"
|
|
1129
|
-
)
|
|
1130
|
-
return 1
|
|
1157
|
+
if getattr(args, "host", "claude-code") == "codex":
|
|
1158
|
+
return cmd_update_codex(args)
|
|
1131
1159
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
return exc.returncode or 1
|
|
1160
|
+
_run_service(_DASHBOARD_SCRIPT, "stop")
|
|
1161
|
+
_run_service(_BACKEND_SCRIPT, "stop")
|
|
1162
|
+
sys.stdout.write("Updating claude-smart by reinstalling from this package...\n")
|
|
1163
|
+
install_args = argparse.Namespace(**vars(args), refresh_existing=True)
|
|
1164
|
+
install_args.host = "claude-code"
|
|
1165
|
+
return cmd_install(install_args)
|
|
1139
1166
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1167
|
+
|
|
1168
|
+
def cmd_update_codex(_args: argparse.Namespace) -> int:
|
|
1169
|
+
"""Update Codex support by refreshing its marketplace/cache install."""
|
|
1170
|
+
plugin_root = _find_codex_plugin_root()
|
|
1142
1171
|
if plugin_root is not None:
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1172
|
+
_run_service(plugin_root / "scripts" / "dashboard-service.sh", "stop")
|
|
1173
|
+
_run_service(plugin_root / "scripts" / "backend-service.sh", "stop")
|
|
1174
|
+
else:
|
|
1175
|
+
_run_service(_DASHBOARD_SCRIPT, "stop")
|
|
1176
|
+
_run_service(_BACKEND_SCRIPT, "stop")
|
|
1177
|
+
sys.stdout.write(
|
|
1178
|
+
"Updating claude-smart Codex support by reinstalling from this package...\n"
|
|
1179
|
+
)
|
|
1180
|
+
install_args = argparse.Namespace(**vars(_args))
|
|
1181
|
+
install_args.host = "codex"
|
|
1182
|
+
return cmd_install_codex(install_args)
|
|
1150
1183
|
|
|
1151
1184
|
|
|
1152
1185
|
def cmd_uninstall(_args: argparse.Namespace) -> int:
|
|
@@ -1952,14 +1985,15 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1952
1985
|
default="claude-code",
|
|
1953
1986
|
help="Install target host",
|
|
1954
1987
|
)
|
|
1955
|
-
inst.add_argument(
|
|
1956
|
-
"--source",
|
|
1957
|
-
default=_DEFAULT_MARKETPLACE_SOURCE,
|
|
1958
|
-
help="Marketplace ref — GitHub owner/repo, or a local directory path",
|
|
1959
|
-
)
|
|
1960
1988
|
inst.set_defaults(func=cmd_install)
|
|
1961
1989
|
|
|
1962
1990
|
upd = sub.add_parser("update", help="Update claude-smart to the latest version")
|
|
1991
|
+
upd.add_argument(
|
|
1992
|
+
"--host",
|
|
1993
|
+
choices=("claude-code", "codex"),
|
|
1994
|
+
default="claude-code",
|
|
1995
|
+
help="Update target host",
|
|
1996
|
+
)
|
|
1963
1997
|
upd.set_defaults(func=cmd_update)
|
|
1964
1998
|
|
|
1965
1999
|
uni = sub.add_parser("uninstall", help="Remove claude-smart")
|