claude-smart 0.2.40 → 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.40-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"]);
@@ -508,14 +523,14 @@ function runChecked(command, args, options = {}) {
508
523
  });
509
524
  }
510
525
 
511
- function runPluginService(pluginRoot, scriptName, subcommand) {
526
+ function runPluginService(pluginRoot, scriptName, subcommand, envOverrides = {}) {
512
527
  const script = join(pluginRoot, "scripts", scriptName);
513
528
  if (!existsSync(script)) return false;
514
529
  const bash = resolveCommand(isWindows() ? ["bash.exe", "bash"] : ["bash"]);
515
530
  if (!bash) return false;
516
531
  const result = spawnSync(bash, [script, subcommand], {
517
532
  cwd: pluginRoot,
518
- env: runtimeEnv(),
533
+ env: { ...runtimeEnv(), ...envOverrides },
519
534
  stdio: "ignore",
520
535
  windowsHide: true,
521
536
  timeout: PLUGIN_SERVICE_TIMEOUT_MS,
@@ -542,6 +557,12 @@ function refreshDashboardService(pluginRoot) {
542
557
  return runPluginService(pluginRoot, "dashboard-service.sh", "start");
543
558
  }
544
559
 
560
+ function startBackendService(pluginRoot, host) {
561
+ return runPluginService(pluginRoot, "backend-service.sh", "start", {
562
+ CLAUDE_SMART_HOST: host,
563
+ });
564
+ }
565
+
545
566
  function stopClaudeSmartServices(pluginRoot) {
546
567
  runPluginService(pluginRoot, "dashboard-service.sh", "stop");
547
568
  runPluginService(pluginRoot, "backend-service.sh", "stop");
@@ -834,9 +855,7 @@ function patchCodexHooksForNode(pluginRoot, nodePath) {
834
855
  const runner = join(pluginRoot, "scripts", "codex-hook.js");
835
856
  const command = (...args) => [nodePath, runner, ...args].map(quoteCommandPart).join(" ");
836
857
  // Dispatch by command content rather than index — entries can be added or
837
- // reordered (e.g. the SessionStart install hook at index 0) without
838
- // breaking the patch. Entries that must run as bash (smart-install.sh)
839
- // are left untouched.
858
+ // reordered without breaking the patch.
840
859
  const patchOne = (original) => {
841
860
  if (typeof original !== "string") return original;
842
861
  if (original.includes("smart-install.sh")) return original;
@@ -956,13 +975,12 @@ function printHelp() {
956
975
  "Usage:",
957
976
  " npx claude-smart install Install the plugin into Claude Code",
958
977
  " npx claude-smart install --host codex Register the plugin marketplace for Codex",
959
- " npx claude-smart install --source <owner/repo> Override the marketplace source",
960
978
  " npx claude-smart setup Configure managed/read-only/global setup",
961
979
  " npx claude-smart uninstall --host codex Remove the Codex marketplace registration",
962
980
  " npx claude-smart --help Show this help",
963
981
  "",
964
982
  "Claude Code install:",
965
- " 1. claude plugin marketplace add <source>",
983
+ " 1. claude plugin marketplace add <this package>",
966
984
  ` 2. claude plugin install ${PLUGIN_SPEC}`,
967
985
  " 3. Reads ~/.reflexio/.env when managed/read-only setup was configured.",
968
986
  "",
@@ -976,7 +994,8 @@ function printHelp() {
976
994
  " 7. Restart Codex.",
977
995
  "",
978
996
  "Update:",
979
- " 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",
980
999
  " npx claude-smart setup Configure managed/read-only/global setup",
981
1000
  "",
982
1001
  "Uninstall:",
@@ -986,17 +1005,6 @@ function printHelp() {
986
1005
  );
987
1006
  }
988
1007
 
989
- function parseSource(args) {
990
- const idx = args.indexOf("--source");
991
- if (idx === -1) return DEFAULT_MARKETPLACE_SOURCE;
992
- const value = args[idx + 1];
993
- if (!value) {
994
- process.stderr.write("error: --source requires a value (e.g. owner/repo)\n");
995
- process.exit(1);
996
- }
997
- return value;
998
- }
999
-
1000
1008
  function parseHost(args) {
1001
1009
  const idx = args.indexOf("--host");
1002
1010
  if (idx === -1) return "claude-code";
@@ -1392,33 +1400,53 @@ function installCodexPluginCache(pluginRoot) {
1392
1400
  return cacheDir;
1393
1401
  }
1394
1402
 
1395
- async function runUpdate(args) {
1396
- const setup = configureReflexioSetup();
1397
- if (!hasClaudeCli()) {
1398
- process.stderr.write(
1399
- "error: 'claude' CLI not found on PATH. " +
1400
- "Install Claude Code first: https://claude.com/claude-code\n",
1401
- );
1402
- 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.
1403
1418
  }
1404
-
1405
- const code = await runClaude(["plugin", "update", PLUGIN_SPEC], {
1406
- 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
+ }
1407
1427
  });
1408
- if (code !== 0) {
1409
- process.stderr.write(`error: \`claude plugin update ${PLUGIN_SPEC}\` failed (exit ${code})\n`);
1410
- 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;
1411
1435
  }
1412
1436
 
1413
- process.stdout.write("\nclaude-smart updated. Restart Claude Code to apply.\n");
1414
1437
  const pluginRoot = findClaudeCodePluginRoot();
1415
1438
  if (pluginRoot) {
1416
- restorePublishHooksFromSource(pluginRoot);
1417
- if (setup.readOnly) {
1418
- prunePublishHooksForReadOnly(pluginRoot);
1419
- process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
1420
- }
1439
+ stopClaudeSmartServices(pluginRoot);
1421
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);
1422
1450
  }
1423
1451
 
1424
1452
  async function runUninstall(args) {
@@ -1471,7 +1499,7 @@ async function runSetup(args) {
1471
1499
  if (code !== 0) process.exit(code);
1472
1500
  }
1473
1501
 
1474
- async function runInstall(args) {
1502
+ async function runInstall(args, options = {}) {
1475
1503
  if (parseHost(args) === "codex") {
1476
1504
  await runInstallCodex(args);
1477
1505
  return;
@@ -1485,7 +1513,7 @@ async function runInstall(args) {
1485
1513
  process.exit(1);
1486
1514
  }
1487
1515
 
1488
- const source = parseSource(args);
1516
+ const source = PACKAGE_ROOT;
1489
1517
  const setup = configureReflexioSetup();
1490
1518
  const readOnly = setup.readOnly;
1491
1519
 
@@ -1495,7 +1523,21 @@ async function runInstall(args) {
1495
1523
  ];
1496
1524
 
1497
1525
  for (const step of steps) {
1498
- 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
+ }
1499
1541
  if (code !== 0) {
1500
1542
  process.stderr.write(
1501
1543
  `error: \`claude ${step.args.join(" ")}\` failed (exit ${code})\n`,
@@ -1512,6 +1554,9 @@ async function runInstall(args) {
1512
1554
  process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
1513
1555
  }
1514
1556
  process.stdout.write(`Prepared claude-smart runtime at ${pluginRoot}.\n`);
1557
+ if (startBackendService(pluginRoot, "claude-code")) {
1558
+ process.stdout.write("Started claude-smart backend service.\n");
1559
+ }
1515
1560
  if (refreshDashboardService(pluginRoot)) {
1516
1561
  process.stdout.write("Refreshed claude-smart dashboard service.\n");
1517
1562
  }
@@ -1592,6 +1637,9 @@ async function runInstallCodex(args) {
1592
1637
  if (readOnly) {
1593
1638
  process.stdout.write("Installed read-only hook manifest; publish interactions hooks are disabled.\n");
1594
1639
  }
1640
+ if (startBackendService(cacheDir, "codex")) {
1641
+ process.stdout.write("Started claude-smart backend service.\n");
1642
+ }
1595
1643
  if (refreshDashboardService(cacheDir)) {
1596
1644
  process.stdout.write("Refreshed claude-smart dashboard service.\n");
1597
1645
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.40",
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.40",
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.40",
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;
@@ -5,11 +5,6 @@
5
5
  {
6
6
  "matcher": "startup|resume",
7
7
  "hooks": [
8
- {
9
- "type": "command",
10
- "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/smart-install.sh\" || true",
11
- "timeout": 300
12
- },
13
8
  {
14
9
  "type": "command",
15
10
  "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/ensure-plugin-root.sh\" \"$_R\" || true",
@@ -14,16 +14,6 @@
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
- },
27
17
  {
28
18
  "matcher": "startup|clear|compact|resume",
29
19
  "hooks": [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.40"
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,19 +219,25 @@ 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
203
233
  if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
204
- claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; running installer"
205
- CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true
206
- claude_smart_source_login_path
207
- claude_smart_prepend_astral_bins
234
+ claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; starting installer in background"
235
+ claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
236
+ bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
237
+ >>"$STATE_DIR/install.log" 2>&1 || true
208
238
  fi
209
239
  if ! command -v uv >/dev/null 2>&1; then
210
- claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH after installer; skipping"
240
+ claude_smart_append_capped_log "$LOG_FILE" "$LOG_MAX_BYTES" "[claude-smart] backend: uv not on PATH; installer recovery scheduled; skipping"
211
241
  emit_ok; exit 0
212
242
  fi
213
243
  fi
@@ -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 "$@"
@@ -302,33 +302,6 @@ function detached(command, args, options = {}) {
302
302
  return child.pid;
303
303
  }
304
304
 
305
- function runInstaller(root, reason) {
306
- if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false;
307
- const script = path.join(root, "scripts", "smart-install.sh");
308
- if (!fs.existsSync(script)) return false;
309
- const bash = bashPath();
310
- if (!bash) return false;
311
- appendLog("backend.log", `[claude-smart] ${reason}; running installer`);
312
- const result = spawnSync(bash, [script], {
313
- cwd: root,
314
- env: {
315
- ...process.env,
316
- CLAUDE_SMART_BOOTSTRAPPING: "1",
317
- },
318
- encoding: "utf8",
319
- maxBuffer: 20 * 1024 * 1024,
320
- windowsHide: true,
321
- });
322
- const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
323
- if (output) {
324
- ensureDir(STATE_DIR);
325
- fs.appendFileSync(path.join(STATE_DIR, "install.log"), `${output}\n`);
326
- trimLog(path.join(STATE_DIR, "install.log"));
327
- }
328
- prependRuntimePath();
329
- return result.status === 0;
330
- }
331
-
332
305
  function startInstallerDetached(root, reason) {
333
306
  if (process.env.CLAUDE_SMART_BOOTSTRAPPING === "1") return false;
334
307
  const script = path.join(root, "scripts", "smart-install.sh");
@@ -410,11 +383,11 @@ async function startBackend(root) {
410
383
  }
411
384
  const uv = uvPath();
412
385
  if (!uv) {
413
- runInstaller(root, "backend: uv not on PATH");
386
+ startInstallerDetached(root, "backend: uv not on PATH");
414
387
  }
415
388
  const readyUv = uvPath();
416
389
  if (!readyUv) {
417
- appendLog("backend.log", "[claude-smart] backend: uv not on PATH after installer; skipping");
390
+ appendLog("backend.log", "[claude-smart] backend: uv not on PATH; installer recovery scheduled; skipping");
418
391
  emitOk();
419
392
  return;
420
393
  }
@@ -478,11 +451,11 @@ async function startDashboard(root) {
478
451
  }
479
452
  const npm = npmPath();
480
453
  if (!npm) {
481
- runInstaller(root, "dashboard: npm not on PATH");
454
+ startInstallerDetached(root, "dashboard: npm not on PATH");
482
455
  }
483
456
  const readyNpm = npmPath();
484
457
  if (!readyNpm) {
485
- appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH after installer; skipping");
458
+ appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH; installer recovery scheduled; skipping");
486
459
  emitOk();
487
460
  return;
488
461
  }
@@ -512,14 +485,10 @@ function runHook(root, event) {
512
485
  trimLog(path.join(STATE_DIR, "backend.log"));
513
486
  let uv = uvPath();
514
487
  if (!uv) {
515
- if (event === "session-start") {
516
- runInstaller(root, "hook: uv not on PATH");
517
- uv = uvPath();
518
- } else {
519
- startInstallerDetached(root, "hook: uv not on PATH");
520
- }
488
+ startInstallerDetached(root, "hook: uv not on PATH");
489
+ uv = uvPath();
521
490
  if (!uv) {
522
- appendLog("backend.log", "[claude-smart] hook: uv not on PATH after installer; skipping");
491
+ appendLog("backend.log", "[claude-smart] hook: uv not on PATH; installer recovery scheduled; skipping");
523
492
  emitHookOk();
524
493
  return 0;
525
494
  }
@@ -190,14 +190,13 @@ case "$CMD" in
190
190
  NPM_BIN=$(claude_smart_resolve_npm || true)
191
191
  if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
192
192
  if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" != "1" ] && [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
193
- echo "[claude-smart] dashboard: npm is not on PATH; running installer" >>"$LOG_FILE"
194
- CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >>"$STATE_DIR/install.log" 2>&1 || true
195
- claude_smart_source_login_path
196
- claude_smart_prepend_node_bins
197
- NPM_BIN=$(claude_smart_resolve_npm || true)
193
+ echo "[claude-smart] dashboard: npm is not on PATH; starting installer in background" >>"$LOG_FILE"
194
+ claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
195
+ bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
196
+ >>"$STATE_DIR/install.log" 2>&1 || true
198
197
  fi
199
198
  if [ -z "$NPM_BIN" ] || ! "$NPM_BIN" --version >/dev/null 2>&1; then
200
- reason="npm is not on PATH after installer; dashboard cannot start"
199
+ reason="npm is not on PATH; installer recovery scheduled; dashboard cannot start yet"
201
200
  echo "[claude-smart] dashboard: $reason; skipping" >>"$LOG_FILE"
202
201
  claude_smart_write_dashboard_unavailable "$reason"
203
202
  emit_ok; exit 0
@@ -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
@@ -85,29 +85,18 @@ PY
85
85
  fi
86
86
 
87
87
  if ! command -v uv >/dev/null 2>&1; then
88
- # Self-heal from skipped Setup/SessionStart bootstrap. SessionStart can
89
- # afford to wait because it has the install budget; prompt/tool hooks start
90
- # the same installer detached so normal work is not blocked by first-run
91
- # dependency setup.
88
+ # Self-heal from skipped setup without blocking the first prompt. The
89
+ # installer and Setup hook own dependency bootstrap; hooks only schedule
90
+ # recovery and return so SessionStart stays lightweight.
92
91
  if [ "${CLAUDE_SMART_BOOTSTRAPPING:-}" = "1" ]; then
93
92
  claude_smart_emit_continue
94
93
  exit 0
95
94
  fi
96
95
  if [ -x "$PLUGIN_ROOT/scripts/smart-install.sh" ]; then
97
96
  mkdir -p "$STATE_DIR"
98
- if [ "$EVENT" = "session-start" ]; then
99
- CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2
100
- claude_smart_prepend_astral_bins
101
- claude_smart_prepend_node_bins
102
- if command -v uv >/dev/null 2>&1; then
103
- bash "$HERE/backend-service.sh" start >/dev/null 2>&1 || true
104
- bash "$HERE/dashboard-service.sh" start >/dev/null 2>&1 || true
105
- fi
106
- else
107
- claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
108
- bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
109
- >>"$STATE_DIR/install.log" 2>&1 || true
110
- fi
97
+ claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
98
+ bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
99
+ >>"$STATE_DIR/install.log" 2>&1 || true
111
100
  fi
112
101
  if ! command -v uv >/dev/null 2>&1; then
113
102
  claude_smart_emit_continue
@@ -128,17 +117,13 @@ ensure_hook_package_importable() {
128
117
  fi
129
118
 
130
119
  mkdir -p "$STATE_DIR"
131
- if [ "$EVENT" = "session-start" ]; then
132
- CLAUDE_SMART_BOOTSTRAPPING=1 bash "$PLUGIN_ROOT/scripts/smart-install.sh" >&2
133
- else
134
- {
135
- printf '%s\n' "[claude-smart] hook: claude_smart.hook is not importable; retrying install in background"
136
- date 2>/dev/null || true
137
- } >>"$STATE_DIR/install.log" 2>&1
138
- claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
139
- bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
140
- >>"$STATE_DIR/install.log" 2>&1 || true
141
- fi
120
+ {
121
+ printf '%s\n' "[claude-smart] hook: claude_smart.hook is not importable; retrying install in background"
122
+ date 2>/dev/null || true
123
+ } >>"$STATE_DIR/install.log" 2>&1
124
+ claude_smart_spawn_detached env CLAUDE_SMART_BOOTSTRAPPING=1 \
125
+ bash "$PLUGIN_ROOT/scripts/smart-install.sh" \
126
+ >>"$STATE_DIR/install.log" 2>&1 || true
142
127
 
143
128
  claude_smart_python_imports "$PLUGIN_ROOT" claude_smart.hook
144
129
  }
@@ -118,6 +118,13 @@ install_complete() {
118
118
  return 0
119
119
  }
120
120
 
121
+ start_backend_service() {
122
+ if [ -x "$HERE/backend-service.sh" ]; then
123
+ echo "[claude-smart] starting backend service in background" >&2
124
+ bash "$HERE/backend-service.sh" start >/dev/null 2>&1 || true
125
+ fi
126
+ }
127
+
121
128
  install_vendored_reflexio() {
122
129
  local VENDORED_REFLEXIO PLUGIN_PYTHON
123
130
 
@@ -391,6 +398,7 @@ fi
391
398
  preflight_supported_runtime_platform
392
399
 
393
400
  if install_complete; then
401
+ start_backend_service
394
402
  claude_smart_emit_continue
395
403
  exit 0
396
404
  fi
@@ -438,6 +446,22 @@ if ! command -v uv >/dev/null 2>&1; then
438
446
  fi
439
447
 
440
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
441
465
  echo "[claude-smart] running uv sync..." >&2
442
466
  if ! uv sync --locked --python 3.12 --quiet >&2; then
443
467
  write_failure "uv sync failed in $PLUGIN_ROOT — run 'uv sync --locked --python 3.12' there to diagnose"
@@ -618,12 +642,13 @@ elif [ -d "$DASHBOARD_DIR" ]; then
618
642
  fi
619
643
 
620
644
  # Point ~/.reflexio/plugin-root at this install so slash commands can
621
- # reference one stable short path regardless of which marketplace
622
- # (reflexioai or reflexioai-local) loaded us.
645
+ # reference one stable short path regardless of which install loaded us.
623
646
  if ! bash "$HERE/ensure-plugin-root.sh" "$PLUGIN_ROOT"; then
624
647
  echo "[claude-smart] WARNING: failed to set ~/.reflexio/plugin-root symlink — slash commands may not resolve" >&2
625
648
  fi
626
649
 
650
+ start_backend_service
651
+
627
652
  write_success_marker
628
- echo "[claude-smart] install complete. Backend and dashboard auto-start on session start." >&2
653
+ echo "[claude-smart] install complete. Backend started; dashboard auto-starts on session start." >&2
629
654
  claude_smart_emit_continue
@@ -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.40"
422
+ version = "0.2.42"
423
423
  source = { editable = "." }
424
424
  dependencies = [
425
425
  { name = "chromadb" },