claude-smart 0.2.25 → 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.
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: claude-smart
3
+ description: Codex-only — use when the user asks to run claude-smart commands in Codex, including claude-smart show, learn, restart, dashboard, clear-all, or slash-like requests such as /claude-smart:learn. In Claude Code the native plugin slash commands handle these; do not invoke this skill there.
4
+ ---
5
+
6
+ # claude-smart Commands In Codex
7
+
8
+ Codex does not currently support plugin-provided slash commands. When the user
9
+ asks for a claude-smart command, run the equivalent shell command through the
10
+ active plugin root. This skill is Codex-specific; under Claude Code the native
11
+ `/claude-smart:*` slash commands already exist, so do not fall back to the
12
+ shell commands there.
13
+
14
+ ## Command Map
15
+
16
+ - Dashboard: `bash ~/.reflexio/plugin-root/scripts/dashboard-open.sh`
17
+ - Show learned state: `bash ~/.reflexio/plugin-root/scripts/cli.sh show`
18
+ - Learn from the latest turn: `bash ~/.reflexio/plugin-root/scripts/cli.sh learn`
19
+ - Learn with a note: `bash ~/.reflexio/plugin-root/scripts/cli.sh learn --note "<note>"`
20
+ - Restart backend/dashboard: `bash ~/.reflexio/plugin-root/scripts/cli.sh restart`
21
+ - Clear all learned state: `bash ~/.reflexio/plugin-root/scripts/cli.sh clear-all --yes`
22
+
23
+ ## Behavior
24
+
25
+ 1. Treat slash-like prompts such as `/claude-smart:show` as requests to run the
26
+ matching shell command above.
27
+ 2. For `learn`, preserve any user-provided note exactly as the note text.
28
+ 3. For `clear-all`, require explicit confirmation before running it because it
29
+ deletes all reflexio interactions, preferences, and skills.
30
+ 4. If `~/.reflexio/plugin-root` is missing or broken, tell the user to restart
31
+ Codex after installing claude-smart, then rerun the command.
32
+ 5. After running a command, summarize the important output concisely.
@@ -25,6 +25,7 @@ import argparse
25
25
  import json
26
26
  import os
27
27
  import re
28
+ import select
28
29
  import shutil
29
30
  import subprocess
30
31
  import sys
@@ -75,6 +76,8 @@ _CODEX_REQUIRED_FILES = (
75
76
  Path(".agents/plugins/marketplace.json"),
76
77
  Path("plugin/.codex-plugin/plugin.json"),
77
78
  Path("plugin/hooks/codex-hooks.json"),
79
+ Path("plugin/scripts/codex-claude-compat.py"),
80
+ Path("plugin/scripts/codex-hook.js"),
78
81
  Path("plugin/scripts/_codex_env.sh"),
79
82
  )
80
83
  _COPYTREE_IGNORE = shutil.ignore_patterns(
@@ -109,7 +112,10 @@ def _seed_reflexio_env() -> list[str]:
109
112
  _REFLEXIO_ENV_PATH.parent.mkdir(parents=True, exist_ok=True)
110
113
  _REFLEXIO_ENV_PATH.touch(exist_ok=True)
111
114
  existing = _REFLEXIO_ENV_PATH.read_text()
112
- 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
+ )
113
119
  missing = [f for f in flags if f"{f}=" not in existing]
114
120
  if not missing:
115
121
  return []
@@ -337,6 +343,191 @@ def _set_codex_plugin_enabled(path: Path) -> bool:
337
343
  return True
338
344
 
339
345
 
346
+ def _toml_dotted_quoted(name: str) -> str:
347
+ """Quote a TOML bare-key segment whose name may contain ``"`` or ``\\``."""
348
+ return '"' + name.replace("\\", "\\\\").replace('"', '\\"') + '"'
349
+
350
+
351
+ def _set_codex_hook_states(path: Path, states: dict[str, str]) -> bool:
352
+ """Trust and enable Codex hook state entries for the given hook hashes."""
353
+ if not states:
354
+ return False
355
+ if not _remove_toml_sections(
356
+ path,
357
+ exact=set(),
358
+ prefixes=(f'hooks.state."{_CODEX_PLUGIN_ID}:',),
359
+ ):
360
+ return False
361
+ try:
362
+ text = path.read_text() if path.exists() else ""
363
+ except OSError:
364
+ return False
365
+ if text and not text.endswith("\n"):
366
+ text += "\n"
367
+ if "[hooks.state]" not in text:
368
+ if text.strip():
369
+ text += "\n"
370
+ text += "[hooks.state]\n"
371
+ if text.strip():
372
+ text += "\n"
373
+ for key, current_hash in sorted(states.items()):
374
+ text += (
375
+ f"[hooks.state.{_toml_dotted_quoted(key)}]\n"
376
+ "enabled = true\n"
377
+ f'trusted_hash = "{current_hash}"\n\n'
378
+ )
379
+ try:
380
+ path.parent.mkdir(parents=True, exist_ok=True)
381
+ path.write_text(text.rstrip() + "\n")
382
+ except OSError:
383
+ return False
384
+ return True
385
+
386
+
387
+ def _read_codex_app_server_response(
388
+ proc: subprocess.Popen[str], response_id: int, deadline: float
389
+ ) -> dict[str, object]:
390
+ """Read JSON-RPC lines from ``proc.stdout`` until ``response_id`` arrives.
391
+
392
+ Skips unrelated notifications and non-JSON output. Raises ``RuntimeError``
393
+ if the child exits, ``TimeoutError`` once ``deadline`` is reached, or
394
+ re-raises Codex's own error payload as ``RuntimeError``.
395
+ """
396
+ if proc.stdout is None:
397
+ raise RuntimeError("Codex app-server stdout pipe is not available")
398
+ while time.monotonic() < deadline:
399
+ ready, _, _ = select.select([proc.stdout], [], [], 0.2)
400
+ if not ready:
401
+ if proc.poll() is not None:
402
+ raise RuntimeError("Codex app-server exited before responding")
403
+ continue
404
+ line = proc.stdout.readline()
405
+ if not line:
406
+ continue
407
+ try:
408
+ message = json.loads(line)
409
+ except json.JSONDecodeError:
410
+ continue
411
+ if message.get("id") == response_id:
412
+ if "error" in message:
413
+ raise RuntimeError(str(message["error"]))
414
+ return message
415
+ raise TimeoutError("Codex app-server did not respond in time")
416
+
417
+
418
+ def _codex_app_server_request(
419
+ proc: subprocess.Popen[str],
420
+ response_id: int,
421
+ method: str,
422
+ params: dict[str, object],
423
+ deadline: float,
424
+ ) -> dict[str, object]:
425
+ """Send one JSON-RPC request to the Codex app-server and await the response."""
426
+ if proc.stdin is None:
427
+ raise RuntimeError("Codex app-server stdin pipe is not available")
428
+ proc.stdin.write(
429
+ json.dumps({"id": response_id, "method": method, "params": params})
430
+ )
431
+ proc.stdin.write("\n")
432
+ proc.stdin.flush()
433
+ return _read_codex_app_server_response(proc, response_id, deadline)
434
+
435
+
436
+ def _list_codex_plugin_hooks(cwd: Path) -> tuple[bool, list[dict[str, object]], str]:
437
+ """Ask Codex for discovered hook metadata, including current trust hashes."""
438
+ try:
439
+ popen = subprocess.Popen(
440
+ ["codex", "app-server", "--listen", "stdio://"],
441
+ stdin=subprocess.PIPE,
442
+ stdout=subprocess.PIPE,
443
+ stderr=subprocess.DEVNULL,
444
+ text=True,
445
+ )
446
+ except OSError as exc:
447
+ return False, [], f"could not start Codex app-server: {exc}"
448
+
449
+ proc = popen
450
+ try:
451
+ deadline = time.monotonic() + _CODEX_CLI_TIMEOUT_SECONDS
452
+ _codex_app_server_request(
453
+ proc,
454
+ 1,
455
+ "initialize",
456
+ {
457
+ "clientInfo": {
458
+ "name": "claude_smart_installer",
459
+ "title": "claude-smart installer",
460
+ "version": "0.0.0",
461
+ },
462
+ "capabilities": {"experimentalApi": True},
463
+ },
464
+ deadline,
465
+ )
466
+ if proc.stdin is None:
467
+ return False, [], "Codex app-server stdin pipe is not available"
468
+ proc.stdin.write(json.dumps({"method": "initialized", "params": {}}) + "\n")
469
+ proc.stdin.flush()
470
+ response = _codex_app_server_request(
471
+ proc,
472
+ 2,
473
+ "hooks/list",
474
+ {"cwds": [str(cwd)]},
475
+ deadline,
476
+ )
477
+ except (OSError, RuntimeError, TimeoutError) as exc:
478
+ return False, [], str(exc)
479
+ finally:
480
+ proc.terminate()
481
+ try:
482
+ proc.wait(timeout=2)
483
+ except subprocess.TimeoutExpired:
484
+ proc.kill()
485
+ proc.wait(timeout=2)
486
+
487
+ result = response.get("result")
488
+ data = result.get("data") if isinstance(result, dict) else None
489
+ if not isinstance(data, list) or not data:
490
+ return False, [], "Codex app-server returned no hook metadata"
491
+ hooks = data[0].get("hooks") if isinstance(data[0], dict) else None
492
+ if not isinstance(hooks, list):
493
+ return False, [], "Codex app-server hook metadata was malformed"
494
+ plugin_hooks = [
495
+ hook
496
+ for hook in hooks
497
+ if isinstance(hook, dict)
498
+ and (
499
+ hook.get("pluginId") == _CODEX_PLUGIN_ID
500
+ or str(hook.get("key", "")).startswith(f"{_CODEX_PLUGIN_ID}:")
501
+ )
502
+ ]
503
+ return True, plugin_hooks, f"found {len(plugin_hooks)} claude-smart hooks"
504
+
505
+
506
+ def _trust_codex_plugin_hooks(cwd: Path) -> tuple[bool, str]:
507
+ """Seed Codex per-hook trust state for the installed claude-smart plugin."""
508
+ ok, hooks, message = _list_codex_plugin_hooks(cwd)
509
+ if not ok:
510
+ return False, message
511
+ states: dict[str, str] = {}
512
+ for hook in hooks:
513
+ key = hook.get("key")
514
+ current_hash = hook.get("currentHash")
515
+ if (
516
+ isinstance(key, str)
517
+ and key.startswith(f"{_CODEX_PLUGIN_ID}:")
518
+ and isinstance(current_hash, str)
519
+ ):
520
+ states[key] = current_hash
521
+ if not states:
522
+ return False, "Codex did not report trust hashes for claude-smart hooks"
523
+ if not _set_codex_hook_states(_CODEX_CONFIG_PATH, states):
524
+ return (
525
+ False,
526
+ f"could not write claude-smart hook trust state to {_CODEX_CONFIG_PATH}",
527
+ )
528
+ return True, f"trusted and enabled {len(states)} claude-smart Codex hooks"
529
+
530
+
340
531
  def _codex_plugin_version(plugin_root: Path) -> str | None:
341
532
  try:
342
533
  manifest = json.loads(
@@ -401,21 +592,28 @@ def _cleanup_codex_install_state() -> bool:
401
592
 
402
593
 
403
594
  def _enable_codex_plugin_hooks() -> tuple[bool, str]:
404
- """Enable Codex's ``plugin_hooks`` feature, falling back to editing config.toml.
595
+ """Enable Codex hook feature flags, falling back to editing config.toml.
405
596
 
406
597
  Returns:
407
598
  tuple[bool, str]: ``(success, message)``. The message describes
408
- either the successful path taken (CLI vs. direct config write)
409
- or the failure mode.
599
+ either the successful path taken (CLI vs. direct config write)
600
+ or the failure mode.
410
601
  """
411
- result = _run_codex(["features", "enable", "plugin_hooks"])
412
- if result.returncode == 0:
413
- return True, "codex features enable plugin_hooks"
414
- if _set_toml_feature(_CODEX_CONFIG_PATH, "plugin_hooks", True):
415
- return True, f"set plugin_hooks = true in {_CODEX_CONFIG_PATH}"
416
- return False, (
417
- result.stderr or result.stdout or "could not update Codex config"
418
- ).strip()
602
+ messages: list[str] = []
603
+ for feature in ("hooks", "plugin_hooks"):
604
+ result = _run_codex(["features", "enable", feature])
605
+ if result.returncode == 0:
606
+ messages.append(f"codex features enable {feature}")
607
+ continue
608
+ if _set_toml_feature(_CODEX_CONFIG_PATH, feature, True):
609
+ messages.append(f"set {feature} = true in {_CODEX_CONFIG_PATH}")
610
+ continue
611
+ detail = (
612
+ result.stderr or result.stdout or "could not update Codex config"
613
+ ).strip()
614
+ prior = f" (succeeded earlier: {'; '.join(messages)})" if messages else ""
615
+ return False, f"{feature}: {detail}{prior}"
616
+ return True, "; ".join(messages)
419
617
 
420
618
 
421
619
  def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
@@ -501,22 +699,41 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
501
699
 
502
700
  installed = False
503
701
  install_msg = "marketplace registration failed"
702
+ trusted = False
703
+ trust_msg = "plugin was not installed"
504
704
  if registered:
505
705
  installed, install_msg = _install_codex_plugin_cache(
506
706
  marketplace_root / _CODEX_LOCAL_PLUGIN_PATH
507
707
  )
508
708
  if installed:
509
709
  sys.stdout.write(f"{install_msg}.\n")
710
+ trusted, trust_msg = _trust_codex_plugin_hooks(Path.cwd())
711
+ if not trusted:
712
+ # The app-server can race against the just-installed cache.
713
+ # Retry once before giving up; the manual /hooks recovery is
714
+ # available either way.
715
+ time.sleep(0.5)
716
+ trusted, trust_msg = _trust_codex_plugin_hooks(Path.cwd())
717
+ if trusted:
718
+ sys.stdout.write(f"{trust_msg}.\n")
719
+ else:
720
+ sys.stderr.write(f"warning: {trust_msg}\n")
510
721
  else:
511
722
  sys.stderr.write(f"warning: {install_msg}\n")
512
723
 
513
- if registered and installed:
724
+ if registered and installed and trusted:
514
725
  sys.stdout.write(
515
726
  "\nclaude-smart Codex support is installed.\n"
516
- "Restart Codex so the installed plugin and hooks reload. /plugins should "
727
+ "Restart Codex so the installed plugin and trusted hooks reload. /plugins should "
517
728
  f"show claude-smart as installed from the {_CODEX_MARKETPLACE_DISPLAY_NAME} marketplace. "
518
729
  "Uninstall removes the plugin cache and marketplace registration but leaves "
519
- "shared claude-smart data and Codex's global plugin_hooks feature intact.\n"
730
+ "shared claude-smart data and Codex's global hook feature flags intact.\n"
731
+ )
732
+ elif registered and installed:
733
+ sys.stdout.write(
734
+ "\nclaude-smart Codex support is installed, but hook trust could not be completed.\n"
735
+ "Fully quit and reopen Codex in this repo, run /hooks, trust the claude-smart hooks, "
736
+ "and restart Codex so hooks reload.\n"
520
737
  )
521
738
  elif registered:
522
739
  sys.stdout.write(
@@ -531,7 +748,7 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
531
748
  "then fully quit and reopen Codex, run /plugins, install claude-smart from the "
532
749
  f"{_CODEX_MARKETPLACE_DISPLAY_NAME} marketplace, and restart Codex so hooks reload.\n"
533
750
  )
534
- return 0 if hooks_ok and registered and installed else 1
751
+ return 0 if hooks_ok and registered and installed and trusted else 1
535
752
 
536
753
 
537
754
  def cmd_install(args: argparse.Namespace) -> int:
@@ -674,7 +891,7 @@ def cmd_uninstall_codex(_args: argparse.Namespace) -> int:
674
891
  sys.stderr.write(f"warning: could not update {_CODEX_CONFIG_PATH}\n")
675
892
  sys.stdout.write(
676
893
  "claude-smart Codex plugin and marketplace state removed. Restart Codex to apply. "
677
- "Codex's global plugin_hooks feature and local data under ~/.reflexio "
894
+ "Codex's global hook feature flags and local data under ~/.reflexio "
678
895
  "and ~/.claude-smart were left in place.\n"
679
896
  )
680
897
  return 0
@@ -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.25"
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"