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.
- package/README.md +30 -53
- package/bin/claude-smart.js +516 -11
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +2 -1
- package/plugin/hooks/codex-hooks.json +7 -7
- package/plugin/pyproject.toml +2 -1
- package/plugin/scripts/_codex_env.sh +1 -0
- package/plugin/scripts/backend-service.sh +12 -7
- package/plugin/scripts/codex-claude-compat.py +144 -0
- package/plugin/scripts/codex-hook.js +386 -0
- package/plugin/scripts/ensure-plugin-root.sh +3 -2
- package/plugin/scripts/smart-install.sh +0 -1
- package/plugin/skills/claude-smart/SKILL.md +32 -0
- package/plugin/src/claude_smart/cli.py +234 -17
- package/plugin/src/claude_smart/events/stop.py +16 -1
- package/plugin/src/claude_smart/internal_call.py +30 -0
- package/plugin/src/claude_smart/optimizer_assistant.py +86 -6
- package/plugin/uv.lock +12 -1
|
@@ -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 = (
|
|
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
|
|
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
|
-
|
|
409
|
-
|
|
599
|
+
either the successful path taken (CLI vs. direct config write)
|
|
600
|
+
or the failure mode.
|
|
410
601
|
"""
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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 =
|
|
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=
|
|
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 {
|
|
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.
|
|
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"
|