claude-smart 0.2.26 → 0.2.28

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.26-green.svg" alt="Version">
16
+ <img src="https://img.shields.io/badge/version-0.2.28-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">
@@ -12,6 +12,7 @@
12
12
  "use strict";
13
13
 
14
14
  const { execSync, spawn } = require("child_process");
15
+ const crypto = require("crypto");
15
16
  const {
16
17
  appendFileSync,
17
18
  cpSync,
@@ -21,7 +22,8 @@ const {
21
22
  rmSync,
22
23
  writeFileSync,
23
24
  } = require("fs");
24
- const { homedir } = require("os");
25
+ const https = require("https");
26
+ const { homedir, tmpdir } = require("os");
25
27
  const { dirname, join } = require("path");
26
28
 
27
29
  const DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart";
@@ -52,6 +54,8 @@ const CODEX_REQUIRED_FILES = [
52
54
  ".agents/plugins/marketplace.json",
53
55
  "plugin/.codex-plugin/plugin.json",
54
56
  "plugin/hooks/codex-hooks.json",
57
+ "plugin/scripts/codex-claude-compat.py",
58
+ "plugin/scripts/codex-hook.js",
55
59
  "plugin/scripts/_codex_env.sh",
56
60
  ];
57
61
  const CODEX_CLI_TIMEOUT_MS = 30_000;
@@ -175,7 +179,10 @@ function seedReflexioEnv() {
175
179
  const existing = existsSync(REFLEXIO_ENV_PATH)
176
180
  ? readFileSync(REFLEXIO_ENV_PATH, "utf8")
177
181
  : "";
178
- const flags = ["CLAUDE_SMART_USE_LOCAL_CLI", "CLAUDE_SMART_USE_LOCAL_EMBEDDING"];
182
+ const flags = [
183
+ "CLAUDE_SMART_USE_LOCAL_CLI",
184
+ "CLAUDE_SMART_USE_LOCAL_EMBEDDING",
185
+ ];
179
186
  const missing = flags.filter((f) => !new RegExp(`^${f}=`, "m").test(existing));
180
187
  if (missing.length === 0) return [];
181
188
  const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
@@ -184,6 +191,246 @@ function seedReflexioEnv() {
184
191
  return missing;
185
192
  }
186
193
 
194
+ function isWindows() {
195
+ return process.platform === "win32";
196
+ }
197
+
198
+ function runChecked(command, args, options = {}) {
199
+ return new Promise((resolve) => {
200
+ const child = spawn(command, args, {
201
+ cwd: options.cwd,
202
+ env: options.env || process.env,
203
+ shell: isWindows() && /\.(?:cmd|bat)$/i.test(command),
204
+ stdio: "inherit",
205
+ windowsHide: true,
206
+ });
207
+ child.on("exit", (code) => resolve(typeof code === "number" ? code : 1));
208
+ child.on("error", () => resolve(1));
209
+ });
210
+ }
211
+
212
+ function downloadFile(url, dest) {
213
+ return new Promise((resolve, reject) => {
214
+ const request = https.get(url, (response) => {
215
+ if (
216
+ response.statusCode &&
217
+ response.statusCode >= 300 &&
218
+ response.statusCode < 400 &&
219
+ response.headers.location
220
+ ) {
221
+ downloadFile(new URL(response.headers.location, url).toString(), dest)
222
+ .then(resolve, reject);
223
+ response.resume();
224
+ return;
225
+ }
226
+ if (response.statusCode !== 200) {
227
+ response.resume();
228
+ reject(new Error(`download failed (${response.statusCode}) for ${url}`));
229
+ return;
230
+ }
231
+ const chunks = [];
232
+ response.on("data", (chunk) => chunks.push(chunk));
233
+ response.on("end", () => {
234
+ writeFileSync(dest, Buffer.concat(chunks));
235
+ resolve();
236
+ });
237
+ });
238
+ request.on("error", reject);
239
+ request.setTimeout(120_000, () => request.destroy(new Error(`download timed out for ${url}`)));
240
+ });
241
+ }
242
+
243
+ function resolveCommand(names, extraDirs = []) {
244
+ const pathParts = [
245
+ ...extraDirs,
246
+ ...(process.env.PATH || "").split(process.platform === "win32" ? ";" : ":"),
247
+ ].filter(Boolean);
248
+ for (const dir of pathParts) {
249
+ for (const name of names) {
250
+ const candidate = join(dir, name);
251
+ if (existsSync(candidate)) return candidate;
252
+ }
253
+ }
254
+ return null;
255
+ }
256
+
257
+ function privateNodeRoot() {
258
+ return join(homedir(), ".claude-smart", "node", "current");
259
+ }
260
+
261
+ function privateNodeBinDirs() {
262
+ const root = privateNodeRoot();
263
+ return [join(root, "bin"), root];
264
+ }
265
+
266
+ function resolvePrivateNode() {
267
+ return resolveCommand(isWindows() ? ["node.exe", "node"] : ["node"], privateNodeBinDirs());
268
+ }
269
+
270
+ function resolvePrivateNpm() {
271
+ return resolveCommand(
272
+ isWindows() ? ["npm.cmd", "npm.exe", "npm"] : ["npm"],
273
+ privateNodeBinDirs(),
274
+ );
275
+ }
276
+
277
+ function runtimeEnv(extraDirs = []) {
278
+ const delimiter = process.platform === "win32" ? ";" : ":";
279
+ const dirs = [
280
+ ...extraDirs,
281
+ ...privateNodeBinDirs(),
282
+ join(homedir(), ".local", "bin"),
283
+ join(homedir(), ".cargo", "bin"),
284
+ ];
285
+ return {
286
+ ...process.env,
287
+ PATH: `${dirs.join(delimiter)}${delimiter}${process.env.PATH || ""}`,
288
+ };
289
+ }
290
+
291
+ async function ensureWindowsPrivateNode() {
292
+ if (!isWindows()) return null;
293
+ const existing = resolvePrivateNode();
294
+ const existingNpm = resolvePrivateNpm();
295
+ if (existing && existingNpm) return { node: existing, npm: existingNpm };
296
+
297
+ const major = process.env.CLAUDE_SMART_NODE_LTS_MAJOR || "22";
298
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
299
+ const baseUrl = process.env.CLAUDE_SMART_NODE_BASE_URL || `https://nodejs.org/dist/latest-v${major}.x`;
300
+ const nodeRoot = join(homedir(), ".claude-smart", "node");
301
+ const temp = join(tmpdir(), `claude-smart-node-${process.pid}`);
302
+ mkdirSync(nodeRoot, { recursive: true });
303
+ rmSync(temp, { recursive: true, force: true });
304
+ mkdirSync(temp, { recursive: true });
305
+
306
+ const sumsPath = join(temp, "SHASUMS256.txt");
307
+ await downloadFile(`${baseUrl}/SHASUMS256.txt`, sumsPath);
308
+ const sums = readFileSync(sumsPath, "utf8");
309
+ const match = sums
310
+ .split(/\r?\n/)
311
+ .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}`);
314
+ const [expectedHash, archiveName] = match;
315
+ const archivePath = join(temp, archiveName);
316
+ await downloadFile(`${baseUrl}/${archiveName}`, archivePath);
317
+ const actualHash = crypto.createHash("sha256").update(readFileSync(archivePath)).digest("hex");
318
+ if (actualHash !== expectedHash) {
319
+ throw new Error(`Node.js checksum verification failed for ${archiveName}`);
320
+ }
321
+
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
+ const extractDir = join(temp, "extract");
325
+ 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
+ );
337
+ if (code !== 0) throw new Error(`Node.js archive extraction failed for ${archiveName}`);
338
+ const extracted = join(extractDir, archiveName.replace(/\.zip$/, ""));
339
+ const current = privateNodeRoot();
340
+ rmSync(current, { recursive: true, force: true });
341
+ cpSync(extracted, current, { recursive: true, force: true });
342
+ rmSync(temp, { recursive: true, force: true });
343
+
344
+ const node = resolvePrivateNode();
345
+ const npm = resolvePrivateNpm();
346
+ if (!node || !npm) throw new Error("private Node.js install completed but node/npm are not usable");
347
+ return { node, npm };
348
+ }
349
+
350
+ function resolveUv() {
351
+ return resolveCommand(isWindows() ? ["uv.exe", "uv"] : ["uv"], [
352
+ join(homedir(), ".local", "bin"),
353
+ join(homedir(), ".cargo", "bin"),
354
+ ]);
355
+ }
356
+
357
+ async function ensureWindowsUv() {
358
+ let uv = resolveUv();
359
+ 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");
370
+ uv = resolveUv();
371
+ if (!uv) throw new Error("uv install reported success but uv.exe was not found");
372
+ return uv;
373
+ }
374
+
375
+ function quoteCommandPart(part) {
376
+ return `"${String(part).replace(/"/g, '\\"')}"`;
377
+ }
378
+
379
+ function patchCodexHooksForWindows(pluginRoot, nodePath) {
380
+ const hookPath = join(pluginRoot, "hooks", "codex-hooks.json");
381
+ const parsed = JSON.parse(readFileSync(hookPath, "utf8"));
382
+ const runner = join(pluginRoot, "scripts", "codex-hook.js");
383
+ 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");
395
+ writeFileSync(hookPath, JSON.stringify(parsed, null, 2) + "\n");
396
+ }
397
+
398
+ function ensureWindowsPluginRoot(pluginRoot) {
399
+ const reflexioDir = dirname(REFLEXIO_ENV_PATH);
400
+ const link = join(reflexioDir, "plugin-root");
401
+ mkdirSync(reflexioDir, { recursive: true });
402
+ rmSync(link, { recursive: true, force: true });
403
+ try {
404
+ require("fs").symlinkSync(pluginRoot, link, "junction");
405
+ } catch {
406
+ writeFileSync(join(reflexioDir, "plugin-root.txt"), `${pluginRoot}\n`);
407
+ }
408
+ }
409
+
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();
417
+ const env = runtimeEnv([dirname(uv), ...privateNodeBinDirs()]);
418
+ let code = await runChecked(
419
+ uv,
420
+ ["sync", "--locked", "--python", "3.12", "--quiet"],
421
+ { cwd: pluginRoot, env },
422
+ );
423
+ if (code !== 0) throw new Error(`uv sync failed in ${pluginRoot}`);
424
+
425
+ const dashboardDir = join(pluginRoot, "dashboard");
426
+ if (existsSync(dashboardDir)) {
427
+ code = await runChecked(nodeRuntime.npm, ["ci"], { cwd: dashboardDir, env });
428
+ if (code !== 0) throw new Error(`npm ci failed in ${dashboardDir}`);
429
+ code = await runChecked(nodeRuntime.npm, ["run", "build"], { cwd: dashboardDir, env });
430
+ if (code !== 0) throw new Error(`npm run build failed in ${dashboardDir}`);
431
+ }
432
+ }
433
+
187
434
  function printHelp() {
188
435
  process.stdout.write(
189
436
  [
@@ -760,6 +1007,7 @@ async function runInstallCodex() {
760
1007
  try {
761
1008
  cacheDir = installCodexPluginCache(join(marketplaceRoot, CODEX_MARKETPLACE_PLUGIN_PATH));
762
1009
  process.stdout.write(`Installed Codex plugin cache at ${cacheDir}.\n`);
1010
+ await bootstrapWindowsCodexCache(cacheDir);
763
1011
  } catch (err) {
764
1012
  process.stderr.write(
765
1013
  `error: automatic Codex plugin install failed: ${err && err.message ? err.message : err}\n`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-smart",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
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.26",
3
+ "version": "0.2.28",
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.26",
3
+ "version": "0.2.28",
4
4
  "description": "Self-improving coding assistant plugin — learns from corrections across sessions via reflexio",
5
5
  "author": {
6
6
  "name": "Yi Lu"
@@ -7,22 +7,22 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$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",
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",
11
11
  "timeout": 10
12
12
  },
13
13
  {
14
14
  "type": "command",
15
- "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/backend-service.sh\" start",
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/backend-service.sh\" start",
16
16
  "timeout": 30
17
17
  },
18
18
  {
19
19
  "type": "command",
20
- "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/dashboard-service.sh\" start",
20
+ "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/dashboard-service.sh\" start",
21
21
  "timeout": 10
22
22
  },
23
23
  {
24
24
  "type": "command",
25
- "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex session-start",
25
+ "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/hook_entry.sh\" codex session-start",
26
26
  "timeout": 30,
27
27
  "statusMessage": "Loading claude-smart context"
28
28
  }
@@ -34,7 +34,7 @@
34
34
  "hooks": [
35
35
  {
36
36
  "type": "command",
37
- "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex user-prompt",
37
+ "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/hook_entry.sh\" codex user-prompt",
38
38
  "timeout": 15
39
39
  }
40
40
  ]
@@ -46,7 +46,7 @@
46
46
  "hooks": [
47
47
  {
48
48
  "type": "command",
49
- "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex post-tool",
49
+ "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/hook_entry.sh\" codex post-tool",
50
50
  "timeout": 15
51
51
  }
52
52
  ]
@@ -57,7 +57,7 @@
57
57
  "hooks": [
58
58
  {
59
59
  "type": "command",
60
- "command": "_R=\"${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; [ -z \"$_R\" ] && _R=$(ls -dt \"$HOME/plugins/claude-smart\" \"$HOME/.codex/plugins/cache/reflexioai/claude-smart\"/*/ 2>/dev/null | head -n 1); [ -n \"$_R\" ] && . \"${_R%/}/scripts/_codex_env.sh\" && bash \"$_R/scripts/hook_entry.sh\" codex stop",
60
+ "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/hook_entry.sh\" codex stop",
61
61
  "timeout": 30
62
62
  }
63
63
  ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-smart"
3
- version = "0.2.26"
3
+ version = "0.2.28"
4
4
  description = "Self-improving Claude Code plugin — learns from corrections via reflexio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -10,6 +10,7 @@ dependencies = [
10
10
  # Pulls in onnxruntime + tokenizers; the ~80 MB ONNX model itself is
11
11
  # downloaded on first use, not at install time.
12
12
  "chromadb>=0.5",
13
+ "einops>=0.8.0",
13
14
  ]
14
15
 
15
16
  [project.scripts]
@@ -25,3 +25,4 @@ _R="${_R%/}"
25
25
  export _R
26
26
  export PLUGIN_ROOT="$_R"
27
27
  export CLAUDE_PLUGIN_ROOT="$_R"
28
+ export CLAUDE_SMART_HOST="codex"
@@ -29,25 +29,29 @@ PORT=8071
29
29
  # binds to PORT instead of reflexio's library default (8081).
30
30
  export BACKEND_PORT="$PORT"
31
31
 
32
- # Default: route extraction through the local claude CLI + ONNX embedder
32
+ # Default: route extraction through the active host CLI + ONNX embedder
33
33
  # so claude-smart works without any LLM API key. Users can opt out by
34
34
  # pre-exporting these to 0.
35
35
  export CLAUDE_SMART_USE_LOCAL_CLI="${CLAUDE_SMART_USE_LOCAL_CLI:-1}"
36
36
  export CLAUDE_SMART_USE_LOCAL_EMBEDDING="${CLAUDE_SMART_USE_LOCAL_EMBEDDING:-1}"
37
- # The backend can be spawned from contexts whose PATH lacks the claude
37
+ # The backend can be spawned from contexts whose PATH lacks the host
38
38
  # CLI dir (commonly ~/.local/bin or /opt/homebrew/bin). Pin the CLI
39
39
  # explicitly if we can resolve it from our own (post-login-path) PATH.
40
+ PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
41
+
40
42
  if [ -z "${CLAUDE_SMART_CLI_PATH:-}" ]; then
41
- if _cs_claude_path=$(command -v claude 2>/dev/null) && [ -n "$_cs_claude_path" ]; then
42
- export CLAUDE_SMART_CLI_PATH="$_cs_claude_path"
43
+ if [ "${CLAUDE_SMART_HOST:-claude-code}" = "codex" ]; then
44
+ # Prefer the compatibility executable for older provider entrypoints; the
45
+ # provider can also resolve `codex` directly when this override is absent.
46
+ export CLAUDE_SMART_CLI_PATH="$PLUGIN_ROOT/scripts/codex-claude-compat.py"
47
+ elif _cs_cli_path=$(command -v claude 2>/dev/null) && [ -n "$_cs_cli_path" ]; then
48
+ export CLAUDE_SMART_CLI_PATH="$_cs_cli_path"
43
49
  elif [ -x "$HOME/.local/bin/claude" ]; then
44
50
  export CLAUDE_SMART_CLI_PATH="$HOME/.local/bin/claude"
45
51
  fi
46
- unset _cs_claude_path
52
+ unset _cs_cli_path
47
53
  fi
48
54
 
49
- PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
50
-
51
55
  STATE_DIR="$HOME/.claude-smart"
52
56
  PID_FILE="$STATE_DIR/backend.pid"
53
57
  LOG_FILE="$STATE_DIR/backend.log"
@@ -55,6 +59,37 @@ mkdir -p "$STATE_DIR"
55
59
 
56
60
  emit_ok() { echo '{"continue":true,"suppressOutput":true}'; }
57
61
 
62
+ emit_start_failure() {
63
+ reason="$1"
64
+ if py=$(claude_smart_resolve_python 2>/dev/null); then
65
+ "$py" - "$reason" <<'PY'
66
+ import json
67
+ import sys
68
+
69
+ reason = sys.argv[1].strip()
70
+ message = (
71
+ "> **claude-smart learning backend is not running.** "
72
+ "Interactions are being buffered locally, but learning will not publish "
73
+ "until the backend starts.\n"
74
+ )
75
+ if reason:
76
+ message += f">\n> Last startup error: `{reason}`\n"
77
+ message += (
78
+ ">\n> Make sure the local model provider is available: Claude Code needs "
79
+ "`claude`, Codex needs `codex`. Then run `/claude-smart:restart`."
80
+ )
81
+ print(json.dumps({
82
+ "hookSpecificOutput": {
83
+ "hookEventName": "SessionStart",
84
+ "additionalContext": message,
85
+ }
86
+ }))
87
+ PY
88
+ else
89
+ emit_ok
90
+ fi
91
+ }
92
+
58
93
  # Tree-kill the recorded process. Delegates to claude_smart_kill_tree
59
94
  # (POSIX: signal the process group; Windows: taskkill /T /F /PID).
60
95
  kill_group() {
@@ -184,6 +219,14 @@ case "$CMD" in
184
219
  backend_healthy && break
185
220
  sleep 1
186
221
  done
222
+ if ! backend_healthy; then
223
+ pid=$(cat "$PID_FILE" 2>/dev/null || echo "")
224
+ if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
225
+ reason=$(tail -n 120 "$LOG_FILE" 2>/dev/null | grep -E "No LLM provider available|No generation-capable LLM provider available|CLI not found|skipping provider registration|Application startup failed" | tail -n 1 | sed 's/^[[:space:]]*//')
226
+ emit_start_failure "$reason"
227
+ exit 0
228
+ fi
229
+ fi
187
230
  emit_ok
188
231
  ;;
189
232
  stop)
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+ """Translate Reflexio's Claude CLI provider contract to ``codex exec``.
3
+
4
+ Reflexio's local provider currently shells out to ``CLAUDE_SMART_CLI_PATH`` as
5
+ if it were the Claude Code CLI:
6
+
7
+ <path> -p --output-format json --model <model> [--append-system-prompt ...]
8
+
9
+ When claude-smart runs under Codex, this executable preserves that narrow
10
+ contract while routing the actual model call through ``codex exec``. The
11
+ stdout shape intentionally matches Claude Code's JSON output enough for
12
+ Reflexio's provider to read ``result``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import shutil
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ from pathlib import Path
24
+
25
+
26
+ _TIMEOUT_SECONDS = 120
27
+
28
+
29
+ def main(argv: list[str] | None = None) -> int:
30
+ argv = list(sys.argv[1:] if argv is None else argv)
31
+ try:
32
+ output_format, system_prompt = _parse_supported_args(argv)
33
+ content = _run_codex(
34
+ prompt=sys.stdin.read(),
35
+ system_prompt=system_prompt,
36
+ )
37
+ except Exception as exc: # noqa: BLE001 - CLI bridge errors go to stderr.
38
+ print(f"codex-claude-compat: {exc}", file=sys.stderr)
39
+ return 1
40
+
41
+ payload = {"result": content}
42
+ if output_format == "stream-json":
43
+ payload = {"type": "result", "subtype": "success", "result": content}
44
+ json.dump(payload, sys.stdout, ensure_ascii=False)
45
+ sys.stdout.write("\n")
46
+ return 0
47
+
48
+
49
+ def _parse_supported_args(argv: list[str]) -> tuple[str, str]:
50
+ output_format = "json"
51
+ system_prompt = ""
52
+ idx = 0
53
+ while idx < len(argv):
54
+ arg = argv[idx]
55
+ if arg == "-p":
56
+ idx += 1
57
+ elif arg == "--output-format":
58
+ if idx + 1 >= len(argv):
59
+ raise ValueError("--output-format requires a value")
60
+ output_format = argv[idx + 1]
61
+ idx += 2
62
+ elif arg == "--model":
63
+ idx += 2
64
+ elif arg in {"--verbose", "--include-partial-messages"}:
65
+ idx += 1
66
+ elif arg == "--append-system-prompt":
67
+ if idx + 1 >= len(argv):
68
+ raise ValueError("--append-system-prompt requires a value")
69
+ system_prompt = argv[idx + 1]
70
+ idx += 2
71
+ else:
72
+ raise ValueError(f"unsupported Claude CLI argument: {arg}")
73
+ if output_format not in {"json", "stream-json"}:
74
+ raise ValueError(f"unsupported --output-format: {output_format}")
75
+ return output_format, system_prompt
76
+
77
+
78
+ def _run_codex(*, prompt: str, system_prompt: str) -> str:
79
+ codex_path = os.environ.get("CLAUDE_SMART_CODEX_PATH") or shutil.which("codex")
80
+ if not codex_path:
81
+ raise FileNotFoundError("codex CLI not found on PATH")
82
+
83
+ output_path = _temporary_output_path()
84
+ cmd = [
85
+ codex_path,
86
+ "exec",
87
+ "--sandbox",
88
+ "read-only",
89
+ "--skip-git-repo-check",
90
+ "--ephemeral",
91
+ "--ignore-rules",
92
+ "--output-last-message",
93
+ str(output_path),
94
+ "-",
95
+ ]
96
+
97
+ env = os.environ.copy()
98
+ env["CLAUDE_SMART_HOST"] = "codex"
99
+ env["CLAUDE_SMART_INTERNAL"] = "1"
100
+ env["CLAUDE_CODE_ENTRYPOINT"] = "optimizer"
101
+
102
+ try:
103
+ proc = subprocess.run( # noqa: S603 - fixed command plus resolved executable.
104
+ cmd,
105
+ input=_codex_prompt(prompt=prompt, system_prompt=system_prompt),
106
+ capture_output=True,
107
+ text=True,
108
+ timeout=_TIMEOUT_SECONDS,
109
+ check=False,
110
+ env=env,
111
+ )
112
+ if proc.returncode != 0:
113
+ stderr = proc.stderr.strip()
114
+ raise RuntimeError(f"codex CLI exited {proc.returncode}: {stderr[:500]}")
115
+ content = output_path.read_text(encoding="utf-8").strip()
116
+ except subprocess.TimeoutExpired as exc:
117
+ raise TimeoutError(f"codex CLI timed out after {_TIMEOUT_SECONDS}s") from exc
118
+ finally:
119
+ try:
120
+ output_path.unlink()
121
+ except OSError:
122
+ pass
123
+
124
+ if not content:
125
+ raise RuntimeError("codex CLI returned empty output")
126
+ return content
127
+
128
+
129
+ def _temporary_output_path() -> Path:
130
+ handle = tempfile.NamedTemporaryFile(prefix="claude-smart-codex-", delete=False)
131
+ try:
132
+ return Path(handle.name)
133
+ finally:
134
+ handle.close()
135
+
136
+
137
+ def _codex_prompt(*, prompt: str, system_prompt: str) -> str:
138
+ if not system_prompt:
139
+ return prompt
140
+ return f"{system_prompt}\n\n## Task\n{prompt}"
141
+
142
+
143
+ if __name__ == "__main__":
144
+ raise SystemExit(main())