claude-smart 0.2.27 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.27-green.svg" alt="Version">
16
+ <img src="https://img.shields.io/badge/version-0.2.29-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">
@@ -98,6 +98,8 @@ claude plugin install claude-smart@reflexioai
98
98
  The plugin Setup hook installs its own `uv`, Python 3.12 environment, and
99
99
  private Node.js/npm runtime under `~/.claude-smart/` when they are missing, so
100
100
  you do not need to install Python or Node globally for the plugin/dashboard.
101
+ On native Windows, Claude Code hooks still need a Git Bash-compatible `bash`
102
+ until Claude Code exposes a single cross-platform hook command shape.
101
103
 
102
104
  To uninstall:
103
105
 
@@ -121,6 +123,11 @@ Then fully quit and reopen Codex so hooks reload.
121
123
 
122
124
  Requires the `codex` CLI on `PATH` and Node.js (for `npx`).
123
125
 
126
+ The `npx` command needs Node.js only to launch the installer. The installed
127
+ plugin uses a private Node.js/npm runtime and a `uv`-managed Python 3.12
128
+ environment under `~/.claude-smart/`, so hooks and the dashboard do not depend
129
+ on global Python, uv, Node, or npm after install.
130
+
124
131
  To uninstall:
125
132
 
126
133
  ```bash
@@ -133,6 +140,19 @@ Developing the plugin itself? See [DEVELOPER.md](./DEVELOPER.md#developing-local
133
140
 
134
141
  > **Not supported:** Claude Code Cowork, claude.ai/code web, or remote Codex environments without local plugin hooks — they run outside your local machine, so the local backend/dashboard and `~/.reflexio/` aren't reachable.
135
142
 
143
+ ### Vanilla OS support
144
+
145
+ | Platform | Status | Notes |
146
+ | --- | --- | --- |
147
+ | Apple Silicon macOS 14+ | Supported | Runtime bootstrap installs private Node/npm, uv, Python 3.12 deps, and dashboard deps. |
148
+ | Windows x64 | Supported | Runtime bootstrap uses PowerShell for uv/Node archive extraction and patches Codex hooks to the Node wrapper. |
149
+ | Linux | Supported when host hooks are local | Existing Linux behavior is preserved; install coverage depends on available Python wheels. |
150
+ | Intel Mac, macOS 13 or older, Windows ARM | Not supported | Current local embedding/ML dependencies do not publish a complete native wheel set for these targets. |
151
+
152
+ Network access is required during first install for npm, PyPI/uv, Node.js, and
153
+ the first local embedding model download. The ONNX model cache lives at
154
+ `~/.cache/chroma/onnx_models/all-MiniLM-L6-v2/`.
155
+
136
156
  ---
137
157
 
138
158
  ## Key Features
@@ -216,6 +236,7 @@ Advanced users can tune claude-smart via environment variables — see [DEVELOPE
216
236
  | `~/.codex/plugins/cache/reflexioai/claude-smart/<version>/` | Codex's cached install of the `claude-smart` plugin from the `ReflexioAI` marketplace. |
217
237
  | `~/.reflexio/plugin-root` | Self-healed symlink to the active plugin dir (managed by `ensure-plugin-root.sh` — written on install, refreshed each `SessionStart`). Claude Code slash commands and Codex shell-command helpers resolve through it, so don't delete it; if you do, the next session will recreate it. |
218
238
  | `~/.claude-smart/sessions/{session_id}.jsonl` | Per-session buffer. User turns, assistant turns, tool invocations, `{"published_up_to": N}` watermarks. Safe to inspect and safe to delete — everything past the latest watermark has already been written to reflexio's DB. |
239
+ | `~/.claude-smart/node/current/` | Private Node.js/npm runtime used by hooks and the dashboard after install. |
219
240
  | `~/.cache/chroma/onnx_models/all-MiniLM-L6-v2/` | Cached ONNX weights (~86 MB, downloaded once). Delete to force a re-download. |
220
241
 
221
242
  For troubleshooting, see [TROUBLESHOOTING.md](./TROUBLESHOOTING.md).
@@ -17,13 +17,18 @@ const {
17
17
  appendFileSync,
18
18
  cpSync,
19
19
  existsSync,
20
+ lstatSync,
20
21
  mkdirSync,
21
22
  readFileSync,
23
+ readdirSync,
24
+ renameSync,
22
25
  rmSync,
26
+ statSync,
27
+ symlinkSync,
23
28
  writeFileSync,
24
29
  } = require("fs");
25
30
  const https = require("https");
26
- const { homedir, tmpdir } = require("os");
31
+ const { arch, homedir, platform, release, tmpdir } = require("os");
27
32
  const { dirname, join } = require("path");
28
33
 
29
34
  const DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart";
@@ -32,6 +37,8 @@ const CODEX_MARKETPLACE_NAME = "reflexioai";
32
37
  const CODEX_MARKETPLACE_DISPLAY_NAME = "ReflexioAI";
33
38
  const CODEX_PLUGIN_ID = `claude-smart@${CODEX_MARKETPLACE_NAME}`;
34
39
  const REFLEXIO_ENV_PATH = join(homedir(), ".reflexio", ".env");
40
+ const REFLEXIO_DIR = join(homedir(), ".reflexio");
41
+ const CLAUDE_SMART_STATE_DIR = join(homedir(), ".claude-smart");
35
42
  const CODEX_CONFIG_PATH = join(homedir(), ".codex", "config.toml");
36
43
  const PACKAGE_ROOT = dirname(dirname(__filename));
37
44
  const CODEX_MARKETPLACE_DIR = join(
@@ -54,7 +61,9 @@ const CODEX_REQUIRED_FILES = [
54
61
  ".agents/plugins/marketplace.json",
55
62
  "plugin/.codex-plugin/plugin.json",
56
63
  "plugin/hooks/codex-hooks.json",
57
- "plugin/scripts/codex-claude-compat.py",
64
+ "plugin/scripts/codex-claude-compat",
65
+ "plugin/scripts/codex-claude-compat.cmd",
66
+ "plugin/scripts/codex-claude-compat.js",
58
67
  "plugin/scripts/codex-hook.js",
59
68
  "plugin/scripts/_codex_env.sh",
60
69
  ];
@@ -191,8 +200,132 @@ function seedReflexioEnv() {
191
200
  return missing;
192
201
  }
193
202
 
203
+ function findClaudeCodePluginRoot() {
204
+ const cacheRoot = join(homedir(), ".claude", "plugins", "cache", CODEX_MARKETPLACE_NAME, "claude-smart");
205
+ const candidates = [];
206
+ try {
207
+ for (const entry of readdirSync(cacheRoot, { withFileTypes: true })) {
208
+ if (!entry.isDirectory()) continue;
209
+ const candidate = join(cacheRoot, entry.name);
210
+ if (
211
+ existsSync(join(candidate, "pyproject.toml")) &&
212
+ existsSync(join(candidate, "scripts", "smart-install.sh"))
213
+ ) {
214
+ candidates.push(candidate);
215
+ }
216
+ }
217
+ } catch {
218
+ // Fall through to marketplace/package fallbacks.
219
+ }
220
+ candidates.sort((a, b) => {
221
+ try {
222
+ return statSync(b).mtimeMs - statSync(a).mtimeMs;
223
+ } catch {
224
+ return 0;
225
+ }
226
+ });
227
+ const fallbacks = [
228
+ join(homedir(), ".claude", "plugins", "marketplaces", CODEX_MARKETPLACE_NAME, "plugin"),
229
+ join(PACKAGE_ROOT, "plugin"),
230
+ ];
231
+ for (const candidate of [...candidates, ...fallbacks]) {
232
+ if (
233
+ existsSync(join(candidate, "pyproject.toml")) &&
234
+ existsSync(join(candidate, "scripts", "smart-install.sh"))
235
+ ) {
236
+ return candidate;
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+
242
+ function forcePluginRoot(pluginRoot) {
243
+ mkdirSync(REFLEXIO_DIR, { recursive: true });
244
+ const link = join(REFLEXIO_DIR, "plugin-root");
245
+ try {
246
+ const existing = lstatSync(link);
247
+ if (existing.isSymbolicLink() || existing.isFile()) {
248
+ rmSync(link, { force: true });
249
+ } else {
250
+ throw new Error(`refusing to replace non-symlink plugin-root at ${link}`);
251
+ }
252
+ } catch (err) {
253
+ if (err && err.code !== "ENOENT") throw err;
254
+ }
255
+ try {
256
+ // Use a symlink when possible so slash commands follow the active plugin root.
257
+ symlinkSync(pluginRoot, link, isWindows() ? "junction" : "dir");
258
+ } catch {
259
+ writeFileSync(join(REFLEXIO_DIR, "plugin-root.txt"), `${pluginRoot}\n`);
260
+ }
261
+ }
262
+
263
+ async function bootstrapClaudeCodeInstall() {
264
+ const pluginRoot = findClaudeCodePluginRoot();
265
+ if (!pluginRoot) {
266
+ throw new Error("could not locate installed Claude Code plugin root after install");
267
+ }
268
+ forcePluginRoot(pluginRoot);
269
+ const bash = resolveCommand(isWindows() ? ["bash.exe", "bash"] : ["bash"]);
270
+ if (!bash) {
271
+ throw new Error("bash is required to bootstrap claude-smart dependencies");
272
+ }
273
+ const code = await runChecked(bash, [join(pluginRoot, "scripts", "smart-install.sh")], {
274
+ cwd: pluginRoot,
275
+ });
276
+ if (code !== 0) {
277
+ throw new Error(`smart-install.sh failed in ${pluginRoot}`);
278
+ }
279
+ const failureMarker = join(CLAUDE_SMART_STATE_DIR, "install-failed");
280
+ if (existsSync(failureMarker)) {
281
+ const reason = readFileSync(failureMarker, "utf8").trim() || "unknown error";
282
+ throw new Error(reason);
283
+ }
284
+ return pluginRoot;
285
+ }
286
+
194
287
  function isWindows() {
195
- return process.platform === "win32";
288
+ return currentPlatform() === "win32";
289
+ }
290
+
291
+ function currentPlatform() {
292
+ return process.env.CLAUDE_SMART_TEST_PLATFORM || platform();
293
+ }
294
+
295
+ function currentArch() {
296
+ return process.env.CLAUDE_SMART_TEST_ARCH || arch();
297
+ }
298
+
299
+ function currentRelease() {
300
+ return process.env.CLAUDE_SMART_TEST_RELEASE || release();
301
+ }
302
+
303
+ function platformSupportError() {
304
+ const os = currentPlatform();
305
+ const cpu = currentArch();
306
+ if (os === "darwin") {
307
+ if (cpu !== "arm64") {
308
+ return "claude-smart currently supports Apple Silicon macOS 14+ only; Intel Mac is not supported because native ML wheels are unavailable.";
309
+ }
310
+ const darwinMajor = Number.parseInt(currentRelease().split(".")[0] || "0", 10);
311
+ if (!Number.isFinite(darwinMajor) || darwinMajor < 23) {
312
+ return "claude-smart currently supports macOS 14+ on Apple Silicon; macOS 13 and older are not supported because native ML wheels are unavailable.";
313
+ }
314
+ return null;
315
+ }
316
+ if (os === "win32") {
317
+ if (cpu !== "x64") {
318
+ return "claude-smart currently supports Windows x64 only; Windows ARM is not supported because native ML wheels are unavailable.";
319
+ }
320
+ return null;
321
+ }
322
+ if (os === "linux") return null;
323
+ return "claude-smart currently supports Apple Silicon macOS 14+, Windows x64, and Linux for vanilla installs.";
324
+ }
325
+
326
+ function assertSupportedRuntimePlatform() {
327
+ const message = platformSupportError();
328
+ if (message) throw new Error(message);
196
329
  }
197
330
 
198
331
  function runChecked(command, args, options = {}) {
@@ -243,7 +376,7 @@ function downloadFile(url, dest) {
243
376
  function resolveCommand(names, extraDirs = []) {
244
377
  const pathParts = [
245
378
  ...extraDirs,
246
- ...(process.env.PATH || "").split(process.platform === "win32" ? ";" : ":"),
379
+ ...(process.env.PATH || "").split(isWindows() ? ";" : ":"),
247
380
  ].filter(Boolean);
248
381
  for (const dir of pathParts) {
249
382
  for (const name of names) {
@@ -263,19 +396,26 @@ function privateNodeBinDirs() {
263
396
  return [join(root, "bin"), root];
264
397
  }
265
398
 
399
+ function resolvePrivateCommand(names) {
400
+ for (const dir of privateNodeBinDirs()) {
401
+ for (const name of names) {
402
+ const candidate = join(dir, name);
403
+ if (existsSync(candidate)) return candidate;
404
+ }
405
+ }
406
+ return null;
407
+ }
408
+
266
409
  function resolvePrivateNode() {
267
- return resolveCommand(isWindows() ? ["node.exe", "node"] : ["node"], privateNodeBinDirs());
410
+ return resolvePrivateCommand(isWindows() ? ["node.exe", "node"] : ["node"]);
268
411
  }
269
412
 
270
413
  function resolvePrivateNpm() {
271
- return resolveCommand(
272
- isWindows() ? ["npm.cmd", "npm.exe", "npm"] : ["npm"],
273
- privateNodeBinDirs(),
274
- );
414
+ return resolvePrivateCommand(isWindows() ? ["npm.cmd", "npm.exe", "npm"] : ["npm"]);
275
415
  }
276
416
 
277
417
  function runtimeEnv(extraDirs = []) {
278
- const delimiter = process.platform === "win32" ? ";" : ":";
418
+ const delimiter = isWindows() ? ";" : ":";
279
419
  const dirs = [
280
420
  ...extraDirs,
281
421
  ...privateNodeBinDirs(),
@@ -288,14 +428,35 @@ function runtimeEnv(extraDirs = []) {
288
428
  };
289
429
  }
290
430
 
291
- async function ensureWindowsPrivateNode() {
292
- if (!isWindows()) return null;
431
+ function nodeArchiveSpec() {
432
+ const os = currentPlatform();
433
+ const cpu = currentArch();
434
+ let nodeOs = null;
435
+ let archiveExt = null;
436
+ if (os === "darwin") {
437
+ nodeOs = "darwin";
438
+ archiveExt = "tar.gz";
439
+ } else if (os === "win32") {
440
+ nodeOs = "win";
441
+ archiveExt = "zip";
442
+ } else if (os === "linux") {
443
+ nodeOs = "linux";
444
+ archiveExt = "tar.gz";
445
+ } else {
446
+ throw new Error(`unsupported OS for private Node.js install: ${os}`);
447
+ }
448
+ const nodeArch = cpu === "arm64" ? "arm64" : "x64";
449
+ return { nodeOs, nodeArch, archiveExt };
450
+ }
451
+
452
+ async function ensurePrivateNode() {
293
453
  const existing = resolvePrivateNode();
294
454
  const existingNpm = resolvePrivateNpm();
295
455
  if (existing && existingNpm) return { node: existing, npm: existingNpm };
296
456
 
457
+ assertSupportedRuntimePlatform();
297
458
  const major = process.env.CLAUDE_SMART_NODE_LTS_MAJOR || "22";
298
- const arch = process.arch === "arm64" ? "arm64" : "x64";
459
+ const { nodeOs, nodeArch, archiveExt } = nodeArchiveSpec();
299
460
  const baseUrl = process.env.CLAUDE_SMART_NODE_BASE_URL || `https://nodejs.org/dist/latest-v${major}.x`;
300
461
  const nodeRoot = join(homedir(), ".claude-smart", "node");
301
462
  const temp = join(tmpdir(), `claude-smart-node-${process.pid}`);
@@ -309,8 +470,8 @@ async function ensureWindowsPrivateNode() {
309
470
  const match = sums
310
471
  .split(/\r?\n/)
311
472
  .map((line) => line.trim().split(/\s+/))
312
- .find((parts) => parts[1] && new RegExp(`^node-v[^ ]+-win-${arch}\\.zip$`).test(parts[1]));
313
- if (!match) throw new Error(`could not resolve Node.js win-${arch} archive from ${baseUrl}`);
473
+ .find((parts) => parts[1] && new RegExp(`^node-v[^ ]+-${nodeOs}-${nodeArch}\\.${archiveExt.replace(/\./g, "\\.")}$`).test(parts[1]));
474
+ if (!match) throw new Error(`could not resolve Node.js ${nodeOs}-${nodeArch} archive from ${baseUrl}`);
314
475
  const [expectedHash, archiveName] = match;
315
476
  const archivePath = join(temp, archiveName);
316
477
  await downloadFile(`${baseUrl}/${archiveName}`, archivePath);
@@ -319,26 +480,56 @@ async function ensureWindowsPrivateNode() {
319
480
  throw new Error(`Node.js checksum verification failed for ${archiveName}`);
320
481
  }
321
482
 
322
- const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
323
- if (!powershell) throw new Error("PowerShell is required to extract private Node.js on Windows");
324
483
  const extractDir = join(temp, "extract");
325
484
  mkdirSync(extractDir, { recursive: true });
326
- const code = await runChecked(
327
- powershell,
328
- [
329
- "-NoProfile",
330
- "-ExecutionPolicy",
331
- "Bypass",
332
- "-Command",
333
- "$ProgressPreference='SilentlyContinue'; Expand-Archive -LiteralPath $env:ARCHIVE_PATH -DestinationPath $env:DEST_DIR -Force",
334
- ],
335
- { env: { ...process.env, ARCHIVE_PATH: archivePath, DEST_DIR: extractDir } },
336
- );
485
+ let code = 0;
486
+ if (archiveExt === "zip") {
487
+ const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
488
+ if (!powershell) throw new Error("PowerShell is required to extract private Node.js on Windows");
489
+ code = await runChecked(
490
+ powershell,
491
+ [
492
+ "-NoProfile",
493
+ "-ExecutionPolicy",
494
+ "Bypass",
495
+ "-Command",
496
+ "$ProgressPreference='SilentlyContinue'; Expand-Archive -LiteralPath $env:ARCHIVE_PATH -DestinationPath $env:DEST_DIR -Force",
497
+ ],
498
+ { env: { ...process.env, ARCHIVE_PATH: archivePath, DEST_DIR: extractDir } },
499
+ );
500
+ } else {
501
+ const tar = resolveCommand(["tar"]);
502
+ if (!tar) throw new Error("tar is required to extract private Node.js on macOS");
503
+ code = await runChecked(tar, ["-xzf", archivePath, "-C", extractDir]);
504
+ }
337
505
  if (code !== 0) throw new Error(`Node.js archive extraction failed for ${archiveName}`);
338
- const extracted = join(extractDir, archiveName.replace(/\.zip$/, ""));
506
+ const extracted = join(extractDir, archiveName.replace(/\.zip$/, "").replace(/\.tar\.gz$/, ""));
339
507
  const current = privateNodeRoot();
340
- rmSync(current, { recursive: true, force: true });
341
- cpSync(extracted, current, { recursive: true, force: true });
508
+ // Atomic swap with rollback: move existing `current` to a backup first
509
+ // so a non-EXDEV failure (EACCES, EBUSY) does not leave the user with no
510
+ // private node at all. EXDEV (cross-device) falls back to cpSync.
511
+ const backup = `${current}.prev.${process.pid}`;
512
+ rmSync(backup, { recursive: true, force: true });
513
+ const hadCurrent = existsSync(current);
514
+ if (hadCurrent) renameSync(current, backup);
515
+ try {
516
+ try {
517
+ renameSync(extracted, current);
518
+ } catch (err) {
519
+ if (!err || err.code !== "EXDEV") throw err;
520
+ cpSync(extracted, current, {
521
+ recursive: true,
522
+ force: true,
523
+ verbatimSymlinks: true,
524
+ });
525
+ }
526
+ } catch (err) {
527
+ if (hadCurrent) {
528
+ try { renameSync(backup, current); } catch { /* leave backup for manual recovery */ }
529
+ }
530
+ throw err;
531
+ }
532
+ rmSync(backup, { recursive: true, force: true });
342
533
  rmSync(temp, { recursive: true, force: true });
343
534
 
344
535
  const node = resolvePrivateNode();
@@ -354,21 +545,33 @@ function resolveUv() {
354
545
  ]);
355
546
  }
356
547
 
357
- async function ensureWindowsUv() {
548
+ async function ensureUv() {
358
549
  let uv = resolveUv();
359
550
  if (uv) return uv;
360
- const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
361
- if (!powershell) throw new Error("PowerShell is required to install uv on Windows");
362
- const code = await runChecked(powershell, [
363
- "-NoProfile",
364
- "-ExecutionPolicy",
365
- "Bypass",
366
- "-Command",
367
- "irm https://astral.sh/uv/install.ps1 | iex",
368
- ]);
369
- if (code !== 0) throw new Error("uv install via PowerShell failed");
551
+ assertSupportedRuntimePlatform();
552
+ let code = 0;
553
+ if (isWindows()) {
554
+ const powershell = resolveCommand(["powershell.exe", "powershell", "pwsh"]);
555
+ if (!powershell) throw new Error("PowerShell is required to install uv on Windows");
556
+ code = await runChecked(powershell, [
557
+ "-NoProfile",
558
+ "-ExecutionPolicy",
559
+ "Bypass",
560
+ "-Command",
561
+ "irm https://astral.sh/uv/install.ps1 | iex",
562
+ ]);
563
+ if (code !== 0) throw new Error("uv install via PowerShell failed");
564
+ } else {
565
+ const installer = join(homedir(), ".claude-smart", "uv-install.sh");
566
+ mkdirSync(dirname(installer), { recursive: true });
567
+ await downloadFile("https://astral.sh/uv/install.sh", installer);
568
+ const sh = resolveCommand(["sh"]);
569
+ if (!sh) throw new Error("sh is required to install uv on macOS");
570
+ code = await runChecked(sh, [installer]);
571
+ if (code !== 0) throw new Error("uv install failed");
572
+ }
370
573
  uv = resolveUv();
371
- if (!uv) throw new Error("uv install reported success but uv.exe was not found");
574
+ if (!uv) throw new Error("uv install reported success but uv was not found");
372
575
  return uv;
373
576
  }
374
577
 
@@ -376,50 +579,83 @@ function quoteCommandPart(part) {
376
579
  return `"${String(part).replace(/"/g, '\\"')}"`;
377
580
  }
378
581
 
379
- function patchCodexHooksForWindows(pluginRoot, nodePath) {
582
+ function patchCodexHooksForNode(pluginRoot, nodePath) {
380
583
  const hookPath = join(pluginRoot, "hooks", "codex-hooks.json");
381
584
  const parsed = JSON.parse(readFileSync(hookPath, "utf8"));
382
585
  const runner = join(pluginRoot, "scripts", "codex-hook.js");
383
586
  const command = (...args) => [nodePath, runner, ...args].map(quoteCommandPart).join(" ");
384
- const sessionHooks = parsed.hooks.SessionStart?.[0]?.hooks || [];
385
- if (sessionHooks[0]) sessionHooks[0].command = command("ensure-root");
386
- if (sessionHooks[1]) sessionHooks[1].command = command("backend");
387
- if (sessionHooks[2]) sessionHooks[2].command = command("dashboard");
388
- if (sessionHooks[3]) sessionHooks[3].command = command("hook", "session-start");
389
- const userPrompt = parsed.hooks.UserPromptSubmit?.[0]?.hooks?.[0];
390
- if (userPrompt) userPrompt.command = command("hook", "user-prompt");
391
- const postTool = parsed.hooks.PostToolUse?.[0]?.hooks?.[0];
392
- if (postTool) postTool.command = command("hook", "post-tool");
393
- const stop = parsed.hooks.Stop?.[0]?.hooks?.[0];
394
- if (stop) stop.command = command("hook", "stop");
587
+ // Dispatch by command content rather than index — entries can be added or
588
+ // reordered (e.g. the SessionStart install hook at index 0) without
589
+ // breaking the patch. Entries that must run as bash (smart-install.sh)
590
+ // are left untouched.
591
+ const patchOne = (original) => {
592
+ if (typeof original !== "string") return original;
593
+ if (original.includes("smart-install.sh")) return original;
594
+ if (original.includes("ensure-plugin-root.sh")) return command("ensure-root");
595
+ if (original.includes("backend-service.sh")) return command("backend");
596
+ if (original.includes("dashboard-service.sh")) return command("dashboard");
597
+ // Match `hook_entry.sh" codex session-start` and similar — between
598
+ // the script name, the host token, and the subcommand there may be
599
+ // closing quotes plus whitespace, so allow both as separators.
600
+ const hookMatch = original.match(/hook_entry\.sh\b[\s"']+(?:codex|claude-code)[\s"']+([\w-]+)/);
601
+ if (hookMatch) return command("hook", hookMatch[1]);
602
+ return original;
603
+ };
604
+ for (const event of Object.keys(parsed.hooks || {})) {
605
+ for (const block of parsed.hooks[event] || []) {
606
+ for (const hook of block.hooks || []) {
607
+ hook.command = patchOne(hook.command);
608
+ }
609
+ }
610
+ }
395
611
  writeFileSync(hookPath, JSON.stringify(parsed, null, 2) + "\n");
396
612
  }
397
613
 
398
- function ensureWindowsPluginRoot(pluginRoot) {
614
+ function ensurePluginRoot(pluginRoot) {
399
615
  const reflexioDir = dirname(REFLEXIO_ENV_PATH);
400
616
  const link = join(reflexioDir, "plugin-root");
401
617
  mkdirSync(reflexioDir, { recursive: true });
402
618
  rmSync(link, { recursive: true, force: true });
403
619
  try {
404
- require("fs").symlinkSync(pluginRoot, link, "junction");
620
+ require("fs").symlinkSync(pluginRoot, link, isWindows() ? "junction" : "dir");
405
621
  } catch {
406
622
  writeFileSync(join(reflexioDir, "plugin-root.txt"), `${pluginRoot}\n`);
407
623
  }
408
624
  }
409
625
 
410
- async function bootstrapWindowsCodexCache(pluginRoot) {
411
- if (!isWindows()) return;
412
- process.stdout.write("Preparing Windows Codex runtime for claude-smart hooks...\n");
413
- const nodeRuntime = await ensureWindowsPrivateNode();
414
- patchCodexHooksForWindows(pluginRoot, nodeRuntime.node);
415
- ensureWindowsPluginRoot(pluginRoot);
416
- const uv = await ensureWindowsUv();
626
+ async function bootstrapPluginRuntime(pluginRoot) {
627
+ assertSupportedRuntimePlatform();
628
+ process.stdout.write("Preparing claude-smart runtime for hooks...\n");
629
+ const nodeRuntime = await ensurePrivateNode();
630
+ patchCodexHooksForNode(pluginRoot, nodeRuntime.node);
631
+ ensurePluginRoot(pluginRoot);
632
+ const uv = await ensureUv();
417
633
  const env = runtimeEnv([dirname(uv), ...privateNodeBinDirs()]);
634
+ const pyprojectPath = join(pluginRoot, "pyproject.toml");
635
+ const pyproject = existsSync(pyprojectPath) ? readFileSync(pyprojectPath, "utf8") : "";
636
+ if (/^\s*\[tool\.uv\.sources\]\s*$/m.test(pyproject)) {
637
+ const lockCode = await runChecked(
638
+ uv,
639
+ ["lock", "--quiet"],
640
+ { cwd: pluginRoot, env },
641
+ );
642
+ if (lockCode !== 0) throw new Error(`uv lock failed in ${pluginRoot}`);
643
+ }
418
644
  let code = await runChecked(
419
645
  uv,
420
646
  ["sync", "--locked", "--python", "3.12", "--quiet"],
421
647
  { cwd: pluginRoot, env },
422
648
  );
649
+ if (code !== 0) {
650
+ process.stderr.write(
651
+ `warning: quiet uv sync failed in ${pluginRoot}; retrying with full output.\n`,
652
+ );
653
+ code = await runChecked(
654
+ uv,
655
+ ["sync", "--locked", "--python", "3.12"],
656
+ { cwd: pluginRoot, env },
657
+ );
658
+ }
423
659
  if (code !== 0) throw new Error(`uv sync failed in ${pluginRoot}`);
424
660
 
425
661
  const dashboardDir = join(pluginRoot, "dashboard");
@@ -453,9 +689,10 @@ function printHelp() {
453
689
  ` 1. Copies the bundled marketplace to ${CODEX_MARKETPLACE_DIR}`,
454
690
  " 2. codex plugin marketplace add <copied marketplace>",
455
691
  " 3. codex features enable hooks && codex features enable plugin_hooks",
456
- " 4. Installs claude-smart into Codex's plugin cache and enables it",
457
- " 5. Trusts and enables claude-smart hook entries in ~/.codex/config.toml",
458
- " 6. Restart Codex.",
692
+ " 4. Installs private Node/npm, uv, Python deps, and dashboard deps as needed",
693
+ " 5. Installs claude-smart into Codex's plugin cache and enables it",
694
+ " 6. Trusts and enables claude-smart hook entries in ~/.codex/config.toml",
695
+ " 7. Restart Codex.",
459
696
  "",
460
697
  "Update:",
461
698
  " npx claude-smart update Update to the latest version",
@@ -948,11 +1185,23 @@ async function runInstall(args) {
948
1185
  `Seeded ${REFLEXIO_ENV_PATH} with ${added.join(", ")}.\n`,
949
1186
  );
950
1187
  }
1188
+ try {
1189
+ const pluginRoot = await bootstrapClaudeCodeInstall();
1190
+ process.stdout.write(`Prepared claude-smart runtime at ${pluginRoot}.\n`);
1191
+ } catch (err) {
1192
+ process.stderr.write(
1193
+ `error: claude-smart installed, but dependency bootstrap failed: ${err && err.message ? err.message : err}\n`,
1194
+ );
1195
+ process.stderr.write(
1196
+ "Fix the issue above, then run /claude-smart:restart or restart Claude Code to retry.\n",
1197
+ );
1198
+ process.exit(1);
1199
+ }
951
1200
 
952
1201
  process.stdout.write(
953
1202
  [
954
1203
  "",
955
- "claude-smart installed. Restart Claude Code in your project.",
1204
+ "claude-smart installed and dependencies are prepared. Restart Claude Code in your project.",
956
1205
  "The reflexio backend and dashboard auto-start on session start.",
957
1206
  "Opt out with CLAUDE_SMART_BACKEND_AUTOSTART=0 or CLAUDE_SMART_DASHBOARD_AUTOSTART=0.",
958
1207
  "",
@@ -1007,7 +1256,7 @@ async function runInstallCodex() {
1007
1256
  try {
1008
1257
  cacheDir = installCodexPluginCache(join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH));
1009
1258
  process.stdout.write(`Installed Codex plugin cache at ${cacheDir}.\n`);
1010
- await bootstrapWindowsCodexCache(cacheDir);
1259
+ await bootstrapPluginRuntime(cacheDir);
1011
1260
  } catch (err) {
1012
1261
  process.stderr.write(
1013
1262
  `error: automatic Codex plugin install failed: ${err && err.message ? err.message : err}\n`,
@@ -1111,7 +1360,18 @@ async function main() {
1111
1360
  process.exit(1);
1112
1361
  }
1113
1362
 
1114
- main().catch((err) => {
1115
- process.stderr.write(`claude-smart: ${err && err.message ? err.message : err}\n`);
1116
- process.exit(1);
1117
- });
1363
+ if (require.main === module) {
1364
+ main().catch((err) => {
1365
+ process.stderr.write(`claude-smart: ${err && err.message ? err.message : err}\n`);
1366
+ process.exit(1);
1367
+ });
1368
+ }
1369
+
1370
+ module.exports = {
1371
+ assertSupportedRuntimePlatform,
1372
+ bootstrapPluginRuntime,
1373
+ ensurePrivateNode,
1374
+ ensureUv,
1375
+ patchCodexHooksForNode,
1376
+ platformSupportError,
1377
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
4
4
  "description": "Self-improving Claude Code plugin — learns from corrections via reflexio",
5
5
  "keywords": [
6
6
  "claude",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.27",
3
+ "version": "0.2.29",
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.27",
3
+ "version": "0.2.29",
4
4
  "description": "Self-improving coding assistant plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
package/plugin/README.md CHANGED
@@ -16,6 +16,10 @@ under `~/.claude-smart/` when they are missing. If Node.js is already installed,
16
16
  `npx claude-smart install` is equivalent; if uv is already installed,
17
17
  `uvx claude-smart install` is equivalent.
18
18
 
19
+ Supported vanilla native targets are Apple Silicon macOS 14+ and Windows x64.
20
+ Intel Mac, macOS 13 or older, and Windows ARM fail early because the local
21
+ embedding/ML dependency stack does not provide a complete native wheel set.
22
+
19
23
  Then restart Claude Code.
20
24
 
21
25
  ## Uninstall
@@ -5,6 +5,11 @@
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
+ },
8
13
  {
9
14
  "type": "command",
10
15
  "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",