claude-smart 0.2.26 → 0.2.27

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.27-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.27",
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.27",
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.27",
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.27"
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,30 @@ 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
+ # Reflexio's provider still calls CLAUDE_SMART_CLI_PATH with Claude CLI
45
+ # flags. Use a small compatibility executable that translates that narrow
46
+ # contract to `codex exec`.
47
+ export CLAUDE_SMART_CLI_PATH="$PLUGIN_ROOT/scripts/codex-claude-compat.py"
48
+ elif _cs_cli_path=$(command -v claude 2>/dev/null) && [ -n "$_cs_cli_path" ]; then
49
+ export CLAUDE_SMART_CLI_PATH="$_cs_cli_path"
43
50
  elif [ -x "$HOME/.local/bin/claude" ]; then
44
51
  export CLAUDE_SMART_CLI_PATH="$HOME/.local/bin/claude"
45
52
  fi
46
- unset _cs_claude_path
53
+ unset _cs_cli_path
47
54
  fi
48
55
 
49
- PLUGIN_ROOT="$(cd "$HERE/.." && pwd)"
50
-
51
56
  STATE_DIR="$HOME/.claude-smart"
52
57
  PID_FILE="$STATE_DIR/backend.pid"
53
58
  LOG_FILE="$STATE_DIR/backend.log"
@@ -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())
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { spawn, spawnSync } = require("node:child_process");
5
+ const fs = require("node:fs");
6
+ const http = require("node:http");
7
+ const os = require("node:os");
8
+ const path = require("node:path");
9
+
10
+ const HOME = os.homedir();
11
+ const STATE_DIR = path.join(HOME, ".claude-smart");
12
+ const REFLEXIO_DIR = path.join(HOME, ".reflexio");
13
+ const DEFAULT_BACKEND_PORT = 8071;
14
+ const FALLBACK_BACKEND_PORT = 8072;
15
+ const DASHBOARD_PORT = 3001;
16
+
17
+ function emitOk() {
18
+ process.stdout.write('{"continue":true,"suppressOutput":true}\n');
19
+ }
20
+
21
+ function ensureDir(dir) {
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ }
24
+
25
+ function appendLog(name, line) {
26
+ ensureDir(STATE_DIR);
27
+ fs.appendFileSync(path.join(STATE_DIR, name), `${line}\n`);
28
+ }
29
+
30
+ function pluginRoot() {
31
+ for (const value of [process.env.CLAUDE_PLUGIN_ROOT, process.env.PLUGIN_ROOT]) {
32
+ if (value && fs.existsSync(path.join(value, "pyproject.toml"))) {
33
+ return path.resolve(value);
34
+ }
35
+ }
36
+ const fromScript = path.resolve(__dirname, "..");
37
+ if (fs.existsSync(path.join(fromScript, "pyproject.toml"))) return fromScript;
38
+ const cacheRoot = path.join(HOME, ".codex", "plugins", "cache", "reflexioai", "claude-smart");
39
+ try {
40
+ const versions = fs
41
+ .readdirSync(cacheRoot, { withFileTypes: true })
42
+ .filter((entry) => entry.isDirectory())
43
+ .map((entry) => path.join(cacheRoot, entry.name))
44
+ .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
45
+ for (const candidate of versions) {
46
+ if (fs.existsSync(path.join(candidate, "pyproject.toml"))) return candidate;
47
+ }
48
+ } catch {
49
+ // Fall through to the stable plugin-root link.
50
+ }
51
+ return path.join(REFLEXIO_DIR, "plugin-root");
52
+ }
53
+
54
+ function prependRuntimePath() {
55
+ const privateNode = path.join(STATE_DIR, "node", "current");
56
+ const parts = [
57
+ path.join(privateNode, "bin"),
58
+ privateNode,
59
+ path.join(HOME, ".local", "bin"),
60
+ path.join(HOME, ".cargo", "bin"),
61
+ ];
62
+ process.env.PATH = `${parts.join(path.delimiter)}${path.delimiter}${process.env.PATH || ""}`;
63
+ }
64
+
65
+ function commandPath(names) {
66
+ const pathParts = (process.env.PATH || "").split(path.delimiter).filter(Boolean);
67
+ for (const dir of pathParts) {
68
+ for (const name of names) {
69
+ const candidate = path.join(dir, name);
70
+ if (fs.existsSync(candidate)) return candidate;
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function uvPath() {
77
+ return commandPath(process.platform === "win32" ? ["uv.exe", "uv"] : ["uv"]);
78
+ }
79
+
80
+ function npmPath() {
81
+ return commandPath(process.platform === "win32" ? ["npm.cmd", "npm.exe", "npm"] : ["npm"]);
82
+ }
83
+
84
+ function stateFile(name) {
85
+ return path.join(STATE_DIR, name);
86
+ }
87
+
88
+ function backendUrlFile() {
89
+ return stateFile("backend-url");
90
+ }
91
+
92
+ function writeBackendUrl(port) {
93
+ ensureDir(STATE_DIR);
94
+ fs.writeFileSync(backendUrlFile(), `http://localhost:${port}/\n`);
95
+ }
96
+
97
+ function codexCompatPath(root) {
98
+ return path.join(root, "scripts", "codex-claude-compat.py");
99
+ }
100
+
101
+ function readBackendUrl() {
102
+ if (process.env.REFLEXIO_URL) return process.env.REFLEXIO_URL;
103
+ try {
104
+ const value = fs.readFileSync(backendUrlFile(), "utf8").trim();
105
+ if (value) return value;
106
+ } catch {
107
+ // Fall through to default.
108
+ }
109
+ return `http://localhost:${DEFAULT_BACKEND_PORT}/`;
110
+ }
111
+
112
+ function healthOk(port, pathname, markerHeader) {
113
+ return new Promise((resolve) => {
114
+ const req = http.request(
115
+ {
116
+ host: "127.0.0.1",
117
+ port,
118
+ path: pathname,
119
+ method: "GET",
120
+ timeout: 1200,
121
+ },
122
+ (res) => {
123
+ const ok = res.statusCode && res.statusCode >= 200 && res.statusCode < 400;
124
+ const markerOk = markerHeader ? Boolean(res.headers[markerHeader]) : true;
125
+ res.resume();
126
+ resolve(Boolean(ok && markerOk));
127
+ },
128
+ );
129
+ req.on("timeout", () => req.destroy());
130
+ req.on("error", () => resolve(false));
131
+ req.end();
132
+ });
133
+ }
134
+
135
+ function portOccupied(port) {
136
+ return new Promise((resolve) => {
137
+ const req = http.request(
138
+ {
139
+ host: "127.0.0.1",
140
+ port,
141
+ path: "/",
142
+ method: "GET",
143
+ timeout: 900,
144
+ },
145
+ (res) => {
146
+ res.resume();
147
+ resolve(true);
148
+ },
149
+ );
150
+ req.on("timeout", () => req.destroy());
151
+ req.on("error", (err) => {
152
+ resolve(err && err.code !== "ECONNREFUSED");
153
+ });
154
+ req.end();
155
+ });
156
+ }
157
+
158
+ async function waitForHealth(port, pathname, markerHeader, attempts) {
159
+ for (let i = 0; i < attempts; i += 1) {
160
+ if (await healthOk(port, pathname, markerHeader)) return true;
161
+ await new Promise((resolve) => setTimeout(resolve, 1000));
162
+ }
163
+ return false;
164
+ }
165
+
166
+ function detached(command, args, options = {}) {
167
+ const child = spawn(command, args, {
168
+ cwd: options.cwd,
169
+ env: options.env || process.env,
170
+ detached: true,
171
+ shell: process.platform === "win32" && /\.(?:cmd|bat)$/i.test(command),
172
+ stdio: "ignore",
173
+ windowsHide: true,
174
+ });
175
+ child.unref();
176
+ return child.pid;
177
+ }
178
+
179
+ function readPid(file) {
180
+ try {
181
+ const value = fs.readFileSync(file, "utf8").trim();
182
+ return value ? Number(value) : null;
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ function pidAlive(pid) {
189
+ if (!pid || Number.isNaN(pid)) return false;
190
+ try {
191
+ process.kill(pid, 0);
192
+ return true;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ function writePid(file, pid) {
199
+ ensureDir(path.dirname(file));
200
+ fs.writeFileSync(file, `${pid}\n`);
201
+ }
202
+
203
+ function ensurePluginRoot(root) {
204
+ ensureDir(REFLEXIO_DIR);
205
+ const link = path.join(REFLEXIO_DIR, "plugin-root");
206
+ try {
207
+ fs.rmSync(link, { recursive: true, force: true });
208
+ } catch {
209
+ // Ignore and try to recreate below.
210
+ }
211
+ try {
212
+ fs.symlinkSync(root, link, process.platform === "win32" ? "junction" : "dir");
213
+ } catch {
214
+ fs.writeFileSync(path.join(REFLEXIO_DIR, "plugin-root.txt"), `${root}\n`);
215
+ }
216
+ }
217
+
218
+ async function startBackend(root) {
219
+ if (process.env.CLAUDE_SMART_BACKEND_AUTOSTART === "0") {
220
+ emitOk();
221
+ return;
222
+ }
223
+ const pidFile = path.join(STATE_DIR, "backend.pid");
224
+ for (const port of [DEFAULT_BACKEND_PORT, FALLBACK_BACKEND_PORT]) {
225
+ if (pidAlive(readPid(pidFile)) && await healthOk(port, "/health")) {
226
+ writeBackendUrl(port);
227
+ emitOk();
228
+ return;
229
+ }
230
+ if (await healthOk(port, "/health")) {
231
+ writeBackendUrl(port);
232
+ emitOk();
233
+ return;
234
+ }
235
+ }
236
+ const uv = uvPath();
237
+ if (!uv) {
238
+ appendLog("backend.log", "[claude-smart] backend: uv not on PATH; skipping");
239
+ emitOk();
240
+ return;
241
+ }
242
+ let selectedPort = DEFAULT_BACKEND_PORT;
243
+ if (await portOccupied(DEFAULT_BACKEND_PORT)) {
244
+ appendLog("backend.log", "[claude-smart] backend: port 8071 occupied; trying 8072");
245
+ selectedPort = FALLBACK_BACKEND_PORT;
246
+ }
247
+ const backendUrl = `http://localhost:${selectedPort}/`;
248
+ const env = {
249
+ ...process.env,
250
+ BACKEND_PORT: String(selectedPort),
251
+ REFLEXIO_URL: backendUrl,
252
+ CLAUDE_SMART_USE_LOCAL_CLI: process.env.CLAUDE_SMART_USE_LOCAL_CLI || "1",
253
+ CLAUDE_SMART_USE_LOCAL_EMBEDDING: process.env.CLAUDE_SMART_USE_LOCAL_EMBEDDING || "1",
254
+ CLAUDE_SMART_HOST: "codex",
255
+ CLAUDE_SMART_CLI_PATH: process.env.CLAUDE_SMART_CLI_PATH || codexCompatPath(root),
256
+ INTERACTION_CLEANUP_THRESHOLD: process.env.INTERACTION_CLEANUP_THRESHOLD || "500",
257
+ INTERACTION_CLEANUP_DELETE_COUNT: process.env.INTERACTION_CLEANUP_DELETE_COUNT || "200",
258
+ };
259
+ const pid = detached(
260
+ uv,
261
+ [
262
+ "run",
263
+ "--project",
264
+ root,
265
+ "--quiet",
266
+ "reflexio",
267
+ "services",
268
+ "start",
269
+ "--only",
270
+ "backend",
271
+ "--no-reload",
272
+ ],
273
+ { cwd: root, env },
274
+ );
275
+ writePid(pidFile, pid);
276
+ if (await waitForHealth(selectedPort, "/health", null, 10)) {
277
+ writeBackendUrl(selectedPort);
278
+ }
279
+ emitOk();
280
+ }
281
+
282
+ async function startDashboard(root) {
283
+ if (process.env.CLAUDE_SMART_DASHBOARD_AUTOSTART === "0") {
284
+ emitOk();
285
+ return;
286
+ }
287
+ const dashboard = path.join(root, "dashboard");
288
+ if (!fs.existsSync(dashboard)) {
289
+ emitOk();
290
+ return;
291
+ }
292
+ const pidFile = path.join(STATE_DIR, "dashboard.pid");
293
+ if (
294
+ pidAlive(readPid(pidFile)) &&
295
+ await healthOk(DASHBOARD_PORT, "/api/health", "x-claude-smart-dashboard")
296
+ ) {
297
+ emitOk();
298
+ return;
299
+ }
300
+ const npm = npmPath();
301
+ if (!npm) {
302
+ appendLog("dashboard.log", "[claude-smart] dashboard: npm not on PATH; skipping");
303
+ emitOk();
304
+ return;
305
+ }
306
+ if (!fs.existsSync(path.join(dashboard, ".next"))) {
307
+ const buildPidFile = path.join(STATE_DIR, "dashboard-build.pid");
308
+ if (!pidAlive(readPid(buildPidFile))) {
309
+ const pid = detached(npm, ["run", "build"], { cwd: dashboard });
310
+ writePid(buildPidFile, pid);
311
+ appendLog("dashboard.log", "[claude-smart] dashboard: .next missing; started background build");
312
+ }
313
+ emitOk();
314
+ return;
315
+ }
316
+ const env = {
317
+ ...process.env,
318
+ PORT: String(DASHBOARD_PORT),
319
+ REFLEXIO_URL: readBackendUrl(),
320
+ CLAUDE_SMART_DASHBOARD_WORKSPACE: process.cwd(),
321
+ };
322
+ const pid = detached(npm, ["run", "start"], { cwd: dashboard, env });
323
+ writePid(pidFile, pid);
324
+ await waitForHealth(DASHBOARD_PORT, "/api/health", "x-claude-smart-dashboard", 5);
325
+ emitOk();
326
+ }
327
+
328
+ function runHook(root, event) {
329
+ const uv = uvPath();
330
+ if (!uv) {
331
+ appendLog("backend.log", "[claude-smart] hook: uv not on PATH; skipping");
332
+ emitOk();
333
+ return 0;
334
+ }
335
+ const input = fs.readFileSync(0);
336
+ const result = spawnSync(
337
+ uv,
338
+ ["run", "--project", root, "--quiet", "python", "-m", "claude_smart.hook", "codex", event],
339
+ {
340
+ cwd: root,
341
+ env: {
342
+ ...process.env,
343
+ REFLEXIO_URL: readBackendUrl(),
344
+ CLAUDE_SMART_HOST: "codex",
345
+ CLAUDE_SMART_CLI_PATH: process.env.CLAUDE_SMART_CLI_PATH || codexCompatPath(root),
346
+ },
347
+ input,
348
+ stdio: ["pipe", "inherit", "inherit"],
349
+ windowsHide: true,
350
+ },
351
+ );
352
+ return typeof result.status === "number" ? result.status : 1;
353
+ }
354
+
355
+ async function main() {
356
+ prependRuntimePath();
357
+ const root = pluginRoot();
358
+ process.env.PLUGIN_ROOT = root;
359
+ process.env.CLAUDE_PLUGIN_ROOT = root;
360
+ const action = process.argv[2] || "hook";
361
+ if (action === "ensure-root") {
362
+ ensurePluginRoot(root);
363
+ emitOk();
364
+ return 0;
365
+ }
366
+ if (action === "backend") {
367
+ await startBackend(root);
368
+ return 0;
369
+ }
370
+ if (action === "dashboard") {
371
+ await startDashboard(root);
372
+ return 0;
373
+ }
374
+ if (action === "hook") {
375
+ return runHook(root, process.argv[3] || "session-start");
376
+ }
377
+ emitOk();
378
+ return 0;
379
+ }
380
+
381
+ main()
382
+ .then((code) => process.exit(code))
383
+ .catch((err) => {
384
+ appendLog("backend.log", `[claude-smart] codex hook failed: ${err && err.stack ? err.stack : err}`);
385
+ emitOk();
386
+ });
@@ -51,7 +51,8 @@ if [ "$FOLLOW" = "1" ]; then
51
51
  fi
52
52
 
53
53
  # Cache-tracking: if the link currently resolves to a path under the
54
- # managed plugin cache (~/.claude/plugins/cache/), always retarget it to
54
+ # managed plugin cache (~/.claude/plugins/cache/ or ~/.codex/plugins/cache/),
55
+ # always retarget it to
55
56
  # $TARGET. Plugin updates leave old version directories behind, so a
56
57
  # valid pyproject.toml at the stale target is not proof the link is
57
58
  # fresh. Links pointing outside the cache (e.g., a user's local-dev
@@ -60,7 +61,7 @@ if [ -L "$LINK" ]; then
60
61
  # Literal target string, not realpath: we compare against what was written by ln -s.
61
62
  CURRENT="$(readlink "$LINK" 2>/dev/null || true)"
62
63
  case "$CURRENT" in
63
- "$HOME/.claude/plugins/cache/"*)
64
+ "$HOME/.claude/plugins/cache/"*|"$HOME/.codex/plugins/cache/"*)
64
65
  CURRENT_NORM="${CURRENT%/}"
65
66
  TARGET_NORM="${TARGET%/}"
66
67
  if [ "$CURRENT_NORM" != "$TARGET_NORM" ]; then
@@ -311,7 +311,6 @@ if ! grep -q '^CLAUDE_SMART_USE_LOCAL_EMBEDDING=' "$REFLEXIO_ENV"; then
311
311
  printf '# Use the in-process ONNX embedder (chromadb) — no API key for semantic search\nCLAUDE_SMART_USE_LOCAL_EMBEDDING=1\n' >> "$REFLEXIO_ENV"
312
312
  echo "[claude-smart] appended CLAUDE_SMART_USE_LOCAL_EMBEDDING=1 to $REFLEXIO_ENV" >&2
313
313
  fi
314
-
315
314
  # Migrate stale REFLEXIO_URL from reflexio's library default (8081) to the
316
315
  # plugin backend port (8071). Matches the quoted and unquoted forms but
317
316
  # requires paired quotes, so malformed or deliberately different values
@@ -76,6 +76,8 @@ _CODEX_REQUIRED_FILES = (
76
76
  Path(".agents/plugins/marketplace.json"),
77
77
  Path("plugin/.codex-plugin/plugin.json"),
78
78
  Path("plugin/hooks/codex-hooks.json"),
79
+ Path("plugin/scripts/codex-claude-compat.py"),
80
+ Path("plugin/scripts/codex-hook.js"),
79
81
  Path("plugin/scripts/_codex_env.sh"),
80
82
  )
81
83
  _COPYTREE_IGNORE = shutil.ignore_patterns(
@@ -110,7 +112,10 @@ def _seed_reflexio_env() -> list[str]:
110
112
  _REFLEXIO_ENV_PATH.parent.mkdir(parents=True, exist_ok=True)
111
113
  _REFLEXIO_ENV_PATH.touch(exist_ok=True)
112
114
  existing = _REFLEXIO_ENV_PATH.read_text()
113
- flags = ("CLAUDE_SMART_USE_LOCAL_CLI", "CLAUDE_SMART_USE_LOCAL_EMBEDDING")
115
+ flags = (
116
+ "CLAUDE_SMART_USE_LOCAL_CLI",
117
+ "CLAUDE_SMART_USE_LOCAL_EMBEDDING",
118
+ )
114
119
  missing = [f for f in flags if f"{f}=" not in existing]
115
120
  if not missing:
116
121
  return []
@@ -8,7 +8,7 @@ import time
8
8
  from pathlib import Path
9
9
  from typing import Any
10
10
 
11
- from claude_smart import cs_cite, ids, publish, runtime, state
11
+ from claude_smart import cs_cite, ids, internal_call, publish, runtime, state
12
12
 
13
13
  _LOGGER = logging.getLogger(__name__)
14
14
 
@@ -112,6 +112,16 @@ def _scan_transcript_for_assistant_text(entries: list[dict[str, Any]]) -> str:
112
112
  return "\n\n".join(parts)
113
113
 
114
114
 
115
+ def _scan_transcript_for_user_text(entries: list[dict[str, Any]]) -> str:
116
+ """Return the user text that opened the current transcript turn."""
117
+ for entry in reversed(entries):
118
+ if not _is_user_turn_boundary(entry):
119
+ continue
120
+ message = entry.get("message") or {}
121
+ return "\n\n".join(_extract_text_blocks(message.get("content")))
122
+ return ""
123
+
124
+
115
125
  def _is_user_turn_boundary(entry: dict[str, Any]) -> bool:
116
126
  """True if ``entry`` is the user message that opened the current turn.
117
127
 
@@ -350,6 +360,11 @@ def handle(payload: dict[str, Any]) -> None:
350
360
  if path.is_file():
351
361
  entries = _load_transcript_with_retry(path)
352
362
 
363
+ if runtime.is_codex():
364
+ prompt = payload.get("prompt") or _scan_transcript_for_user_text(entries)
365
+ if internal_call.is_codex_internal_prompt(prompt):
366
+ return
367
+
353
368
  last_assistant_message = payload.get("last_assistant_message")
354
369
  assistant_text = (
355
370
  last_assistant_message
@@ -29,6 +29,9 @@ Detection signals, OR'd:
29
29
  - ``payload.cwd`` resolves inside the reflexio submodule. Catches
30
30
  direct interactive ``claude`` runs from inside reflexio (manual
31
31
  debugging) that would otherwise pollute the corpus.
32
+ - Known Codex-internal prompt templates (title generation and home-screen
33
+ suggestions). These are model calls made by Codex itself, not user
34
+ coding turns, and must never be reflected into claude-smart memory.
32
35
  """
33
36
 
34
37
  from __future__ import annotations
@@ -41,6 +44,19 @@ from claude_smart import runtime
41
44
 
42
45
  _ENTRYPOINT_VAR = "CLAUDE_CODE_ENTRYPOINT"
43
46
  _INTERACTIVE_ENTRYPOINT = "cli"
47
+ _CODEX_TITLE_PROMPT_PREFIX = (
48
+ "You are a helpful assistant. You will be presented with a user prompt, "
49
+ "and your job is to provide a short title for a task"
50
+ )
51
+ _CODEX_SUGGESTIONS_PROMPT_PREFIX = "# Overview\n\nGenerate 0 to 3 "
52
+ _CODEX_SUGGESTIONS_PROMPT_MARKER = (
53
+ "hyperpersonalized suggestions for what this user can do with Codex "
54
+ "in this local project:"
55
+ )
56
+ _CODEX_SUGGESTIONS_APPS_MARKER = (
57
+ "Get an understanding of the user's intent and goals by deeply viewing "
58
+ "their connected apps."
59
+ )
44
60
 
45
61
  # Reflexio submodule lives at <repo>/reflexio when this package runs from
46
62
  # a dev checkout (<repo>/plugin/src/claude_smart/internal_call.py); anchor
@@ -75,6 +91,8 @@ def is_internal_invocation(payload: dict[str, Any]) -> bool:
75
91
  entrypoint = os.environ.get(_ENTRYPOINT_VAR)
76
92
  if entrypoint and entrypoint != _INTERACTIVE_ENTRYPOINT:
77
93
  return True
94
+ if runtime.is_codex() and is_codex_internal_prompt(payload.get("prompt")):
95
+ return True
78
96
  cwd = payload.get("cwd")
79
97
  if not isinstance(cwd, str) or not cwd:
80
98
  return False
@@ -87,3 +105,15 @@ def is_internal_invocation(payload: dict[str, Any]) -> bool:
87
105
  except ValueError:
88
106
  return False
89
107
  return True
108
+
109
+
110
+ def is_codex_internal_prompt(prompt: Any) -> bool:
111
+ """True for Codex's own UI/task prompts, not user-authored turns."""
112
+ if not isinstance(prompt, str):
113
+ return False
114
+ text = prompt.strip()
115
+ return text.startswith(_CODEX_TITLE_PROMPT_PREFIX) or (
116
+ text.startswith(_CODEX_SUGGESTIONS_PROMPT_PREFIX)
117
+ and _CODEX_SUGGESTIONS_PROMPT_MARKER in text
118
+ and _CODEX_SUGGESTIONS_APPS_MARKER in text
119
+ )
@@ -2,22 +2,24 @@
2
2
 
3
3
  Reflexio's ``LocalScriptAssistant`` sends one JSON payload on stdin and expects
4
4
  one JSON object on stdout. This module bridges that protocol to a guarded
5
- ``claude -p`` subprocess so candidate playbooks can be evaluated against Claude
6
- Code without re-entering claude-smart/reflexio hooks.
5
+ local CLI subprocess so candidate playbooks can be evaluated against the active
6
+ host without re-entering claude-smart/reflexio hooks.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
11
  import json
12
12
  import os
13
+ from pathlib import Path
13
14
  import shutil
14
15
  import subprocess
15
16
  import sys
17
+ import tempfile
16
18
  from typing import Any
17
19
 
18
20
  from claude_smart import internal_call, runtime
19
21
 
20
- _CLAUDE_TIMEOUT_SECONDS = 300
22
+ _CLI_TIMEOUT_SECONDS = 300
21
23
  _READ_ONLY_TOOLS = "Read,Grep,Glob,LS"
22
24
  _MUTATING_TOOLS = "Bash,Edit,Write,MultiEdit,NotebookEdit"
23
25
 
@@ -33,7 +35,7 @@ def main() -> int:
33
35
  messages = _validated_list(payload, "messages")
34
36
  playbooks = _validated_list(payload, "playbooks")
35
37
  prompt, system_prompt = _build_prompt(messages, playbooks)
36
- content = _run_claude(prompt=prompt, system_prompt=system_prompt)
38
+ content = _run_local_cli(prompt=prompt, system_prompt=system_prompt)
37
39
  except Exception as exc: # noqa: BLE001 - script errors become LocalScript failures.
38
40
  sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
39
41
  return 1
@@ -134,6 +136,12 @@ def _render_transcript(messages: list[dict[str, str]]) -> str:
134
136
  )
135
137
 
136
138
 
139
+ def _run_local_cli(*, prompt: str, system_prompt: str) -> str:
140
+ if runtime.is_codex():
141
+ return _run_codex(prompt=prompt, system_prompt=system_prompt)
142
+ return _run_claude(prompt=prompt, system_prompt=system_prompt)
143
+
144
+
137
145
  def _run_claude(*, prompt: str, system_prompt: str) -> str:
138
146
  cli_path = shutil.which("claude") or "claude"
139
147
  # This is an evaluation rollout, not a real user session: allow local
@@ -167,13 +175,13 @@ def _run_claude(*, prompt: str, system_prompt: str) -> str:
167
175
  input=prompt,
168
176
  capture_output=True,
169
177
  text=True,
170
- timeout=_CLAUDE_TIMEOUT_SECONDS,
178
+ timeout=_CLI_TIMEOUT_SECONDS,
171
179
  check=False,
172
180
  env=env,
173
181
  )
174
182
  except subprocess.TimeoutExpired as exc:
175
183
  raise OptimizerAssistantError(
176
- f"claude CLI timed out after {_CLAUDE_TIMEOUT_SECONDS}s"
184
+ f"claude CLI timed out after {_CLI_TIMEOUT_SECONDS}s"
177
185
  ) from exc
178
186
  except FileNotFoundError as exc:
179
187
  raise OptimizerAssistantError("claude CLI not found on PATH") from exc
@@ -199,5 +207,77 @@ def _run_claude(*, prompt: str, system_prompt: str) -> str:
199
207
  return content
200
208
 
201
209
 
210
+ def _run_codex(*, prompt: str, system_prompt: str) -> str:
211
+ cli_path = shutil.which("codex") or "codex"
212
+ output_path = _temporary_output_path()
213
+ cmd = [
214
+ cli_path,
215
+ "exec",
216
+ "--sandbox",
217
+ "read-only",
218
+ "--skip-git-repo-check",
219
+ "--ephemeral",
220
+ "--ignore-rules",
221
+ "--output-last-message",
222
+ str(output_path),
223
+ "-",
224
+ ]
225
+
226
+ env = os.environ.copy()
227
+ env[runtime.HOST_ENV] = runtime.HOST_CODEX
228
+ env[runtime.INTERNAL_ENV] = "1"
229
+ env[internal_call._ENTRYPOINT_VAR] = "optimizer" # noqa: SLF001
230
+
231
+ try:
232
+ proc = subprocess.run( # noqa: S603 - command is fixed plus resolved executable.
233
+ cmd,
234
+ input=_codex_prompt(prompt=prompt, system_prompt=system_prompt),
235
+ capture_output=True,
236
+ text=True,
237
+ timeout=_CLI_TIMEOUT_SECONDS,
238
+ check=False,
239
+ env=env,
240
+ )
241
+ except subprocess.TimeoutExpired as exc:
242
+ raise OptimizerAssistantError(
243
+ f"codex CLI timed out after {_CLI_TIMEOUT_SECONDS}s"
244
+ ) from exc
245
+ except FileNotFoundError as exc:
246
+ raise OptimizerAssistantError("codex CLI not found on PATH") from exc
247
+
248
+ try:
249
+ content = output_path.read_text(encoding="utf-8").strip()
250
+ except OSError as exc:
251
+ raise OptimizerAssistantError("codex CLI did not write output") from exc
252
+ finally:
253
+ try:
254
+ output_path.unlink()
255
+ except OSError:
256
+ pass
257
+
258
+ if proc.returncode != 0:
259
+ stderr = proc.stderr.strip()
260
+ raise OptimizerAssistantError(
261
+ f"codex CLI exited {proc.returncode}: {stderr[:500]}"
262
+ )
263
+ if not content:
264
+ raise OptimizerAssistantError("codex CLI returned empty output")
265
+ return content
266
+
267
+
268
+ def _temporary_output_path() -> Path:
269
+ handle = tempfile.NamedTemporaryFile(prefix="claude-smart-codex-", delete=False)
270
+ try:
271
+ return Path(handle.name)
272
+ finally:
273
+ handle.close()
274
+
275
+
276
+ def _codex_prompt(*, prompt: str, system_prompt: str) -> str:
277
+ if not system_prompt:
278
+ return prompt
279
+ return f"{system_prompt}\n\n## Task\n{prompt}"
280
+
281
+
202
282
  if __name__ == "__main__":
203
283
  raise SystemExit(main())
package/plugin/uv.lock CHANGED
@@ -419,10 +419,11 @@ wheels = [
419
419
 
420
420
  [[package]]
421
421
  name = "claude-smart"
422
- version = "0.2.26"
422
+ version = "0.2.27"
423
423
  source = { editable = "." }
424
424
  dependencies = [
425
425
  { name = "chromadb" },
426
+ { name = "einops" },
426
427
  { name = "reflexio-ai" },
427
428
  ]
428
429
 
@@ -434,6 +435,7 @@ dev = [
434
435
  [package.metadata]
435
436
  requires-dist = [
436
437
  { name = "chromadb", specifier = ">=0.5" },
438
+ { name = "einops", specifier = ">=0.8.0" },
437
439
  { name = "reflexio-ai", specifier = ">=0.2.22" },
438
440
  ]
439
441
 
@@ -616,6 +618,15 @@ wheels = [
616
618
  { url = "https://files.pythonhosted.org/packages/51/79/119091c98e2bf49e24ed9f3ae69f816d715d2904aefa6a2baa039a2ba0b0/ecdsa-0.19.2-py2.py3-none-any.whl", hash = "sha256:840f5dc5e375c68f36c1a7a5b9caad28f95daa65185c9253c0c08dd952bb7399", size = 150818, upload-time = "2026-03-26T09:58:15.808Z" },
617
619
  ]
618
620
 
621
+ [[package]]
622
+ name = "einops"
623
+ version = "0.8.2"
624
+ source = { registry = "https://pypi.org/simple" }
625
+ sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" }
626
+ wheels = [
627
+ { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" },
628
+ ]
629
+
619
630
  [[package]]
620
631
  name = "email-validator"
621
632
  version = "2.3.0"