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.
@@ -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.41-green.svg" alt="Version">
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">
@@ -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. For Claude Code it registers the GitHub marketplace and installs the
5
- * plugin. For Codex it copies the bundled local marketplace, registers it,
6
- * and enables plugin hooks. Both paths seed ~/.reflexio/.env with the two
7
- * local-provider flags so reflexio can route generation through local tools
8
- * with no API key. Managed/read-only/global setup is handled by
9
- * `npx claude-smart setup`, which writes ~/.reflexio/.env before running this
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/package fallbacks.
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
- throw new Error("could not locate installed Claude Code plugin root after install");
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 <source>",
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 Update to the latest version",
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
- async function runUpdate(args) {
1400
- const setup = configureReflexioSetup();
1401
- if (!hasClaudeCli()) {
1402
- process.stderr.write(
1403
- "error: 'claude' CLI not found on PATH. " +
1404
- "Install Claude Code first: https://claude.com/claude-code\n",
1405
- );
1406
- process.exit(1);
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
- const code = await runClaude(["plugin", "update", PLUGIN_SPEC], {
1410
- spinnerLabel: "Checking for claude-smart updates…",
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
- if (code !== 0) {
1413
- process.stderr.write(`error: \`claude plugin update ${PLUGIN_SPEC}\` failed (exit ${code})\n`);
1414
- process.exit(code);
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
- restorePublishHooksFromSource(pluginRoot);
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 = parseSource(args);
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
- const code = await runClaude(step.args, { spinnerLabel: step.label });
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.41",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.41",
3
+ "version": "0.2.42",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.41",
3
+ "version": "0.2.42",
4
4
  "description": "Self-improving coding assistant plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -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
- /* config options here */
10
+ turbopack: {
11
+ root: path.resolve(__dirname),
12
+ },
5
13
  };
6
14
 
7
15
  export default nextConfig;
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.41"
3
+ version = "0.2.42"
4
4
  description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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 any reflexio/uvicorn listener still holding $PORT after the PID
142
- # file kill. Filters by cmdline so we don't knock over an unrelated
143
- # service a user has bound to 8071 — symmetric with start's refusal to
144
- # stomp on a foreign listener. Silent on failure.
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:"$PORT" 2>/dev/null) || candidates=""
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
- case "$cmdline" in
153
- *reflexio*|*uvicorn*) ours="$ours $pid" ;;
154
- esac
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
- # Full shutdown: kill the recorded process group (if any) then sweep the
170
- # port for surviving reflexio listeners. Used by both `stop` and the
171
- # opt-in `session-end` path so a stale/missing PID file doesn't produce
172
- # a silent no-op.
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
- claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: port $PORT held by another process; skipping"
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
@@ -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" >&2
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 regardless
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 (used by setup-local-dev.sh)
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. Off by default to preserve
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
- # $TARGET. Plugin updates leave old version directories behind, so a
57
- # valid pyproject.toml at the stale target is not proof the link is
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. This preserves a valid local-dev symlink set earlier by
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 marketplace
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`` followed by ``claude plugin install``,
1059
- then appends the local-provider flags to ``~/.reflexio/.env`` so reflexio
1060
- can route generation through the local Claude Code CLI.
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. Uses ``args.source`` as the
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", args.source],
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 to the latest version via the native plugin CLI.
1145
+ """Update claude-smart by reinstalling from this package.
1116
1146
 
1117
- Runs ``claude plugin update claude-smart@reflexioai``.
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 (unused).
1152
+ args (argparse.Namespace): Parsed CLI args.
1121
1153
 
1122
1154
  Returns:
1123
- int: 0 on success, non-zero if the ``claude`` CLI is missing or fails.
1155
+ int: 0 on success, non-zero if the host CLI is missing or install fails.
1124
1156
  """
1125
- if not shutil.which("claude"):
1126
- sys.stderr.write(
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
- read_only = _configure_reflexio_setup()
1133
- cmd = ["claude", "plugin", "update", _PLUGIN_SPEC]
1134
- try:
1135
- subprocess.run(cmd, check=True)
1136
- except subprocess.CalledProcessError as exc:
1137
- sys.stderr.write(f"error: {' '.join(cmd)} failed (exit {exc.returncode})\n")
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
- sys.stdout.write("\nclaude-smart updated. Restart Claude Code to apply.\n")
1141
- plugin_root = _find_claude_code_plugin_root()
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
- _restore_publish_hooks_from_source(plugin_root)
1144
- if read_only:
1145
- _prune_publish_hooks_for_read_only(plugin_root)
1146
- sys.stdout.write(
1147
- "Installed read-only hook manifest; publish interactions hooks are disabled.\n"
1148
- )
1149
- return 0
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")
package/plugin/uv.lock CHANGED
@@ -419,7 +419,7 @@ wheels = [
419
419
 
420
420
  [[package]]
421
421
  name = "claude-smart"
422
- version = "0.2.41"
422
+ version = "0.2.42"
423
423
  source = { editable = "." }
424
424
  dependencies = [
425
425
  { name = "chromadb" },