claude-smart 0.2.31 → 0.2.33
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 +2 -2
- package/bin/claude-smart.js +222 -20
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/dashboard/app/api/config/route.ts +11 -2
- package/plugin/dashboard/app/api/health/route.ts +45 -6
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +15 -7
- package/plugin/dashboard/app/api/rules/applied/route.ts +27 -0
- package/plugin/dashboard/app/configure/env/page.tsx +36 -31
- package/plugin/dashboard/app/configure/layout.tsx +1 -1
- package/plugin/dashboard/app/configure/server/page.tsx +8 -14
- package/plugin/dashboard/app/dashboard/page.tsx +311 -115
- package/plugin/dashboard/app/globals.css +80 -66
- package/plugin/dashboard/app/layout.tsx +13 -10
- package/plugin/dashboard/app/preferences/[id]/page.tsx +21 -32
- package/plugin/dashboard/app/preferences/page.tsx +154 -54
- package/plugin/dashboard/app/preferences/project/[id]/page.tsx +1 -0
- package/plugin/dashboard/app/rules/[id]/page.tsx +51 -0
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +4 -4
- package/plugin/dashboard/app/sessions/page.tsx +14 -10
- package/plugin/dashboard/app/skills/page.tsx +175 -56
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +22 -38
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +20 -37
- package/plugin/dashboard/components/common/delete-learning-danger-zone.tsx +166 -0
- package/plugin/dashboard/components/common/empty-state.tsx +4 -2
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +5 -3
- package/plugin/dashboard/components/common/page-tabs.tsx +4 -4
- package/plugin/dashboard/components/common/stat-card.tsx +9 -5
- package/plugin/dashboard/components/layout/sidebar.tsx +24 -9
- package/plugin/dashboard/components/layout/top-bar.tsx +37 -25
- package/plugin/dashboard/components/ui/input.tsx +1 -0
- package/plugin/dashboard/hooks/use-settings.tsx +30 -61
- package/plugin/dashboard/hooks/use-stall-state.ts +5 -9
- package/plugin/dashboard/lib/config-file.ts +4 -0
- package/plugin/dashboard/lib/reflexio-client.ts +23 -48
- package/plugin/dashboard/lib/session-reader.ts +222 -6
- package/plugin/dashboard/lib/types.ts +21 -1
- package/plugin/dashboard/package-lock.json +70 -95
- package/plugin/dashboard/package.json +5 -2
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +61 -0
- package/plugin/scripts/backend-service.sh +13 -2
- package/plugin/scripts/cli.sh +1 -0
- package/plugin/scripts/codex-hook.js +101 -3
- package/plugin/scripts/dashboard-service.sh +95 -19
- package/plugin/scripts/hook_entry.sh +13 -7
- package/plugin/scripts/smart-install.sh +18 -13
- package/plugin/src/claude_smart/cli.py +138 -24
- package/plugin/src/claude_smart/context_format.py +244 -6
- package/plugin/src/claude_smart/context_inject.py +8 -1
- package/plugin/src/claude_smart/cs_cite.py +186 -34
- package/plugin/src/claude_smart/env_config.py +103 -0
- package/plugin/src/claude_smart/events/stop.py +32 -6
- package/plugin/src/claude_smart/ids.py +32 -0
- package/plugin/src/claude_smart/internal_call.py +30 -0
- package/plugin/src/claude_smart/publish.py +8 -3
- package/plugin/src/claude_smart/reflexio_adapter.py +106 -29
- package/plugin/uv.lock +1 -1
|
@@ -30,13 +30,15 @@ import shutil
|
|
|
30
30
|
import subprocess
|
|
31
31
|
import sys
|
|
32
32
|
import time
|
|
33
|
+
import uuid
|
|
33
34
|
from dataclasses import dataclass
|
|
34
35
|
from pathlib import Path
|
|
35
36
|
|
|
36
|
-
from claude_smart import context_format, ids, publish, state
|
|
37
|
+
from claude_smart import context_format, env_config, ids, publish, state
|
|
37
38
|
from claude_smart.reflexio_adapter import Adapter
|
|
38
39
|
|
|
39
|
-
_REFLEXIO_ENV_PATH =
|
|
40
|
+
_REFLEXIO_ENV_PATH = env_config.REFLEXIO_ENV_PATH
|
|
41
|
+
_MANAGED_REFLEXIO_URL = env_config.MANAGED_REFLEXIO_URL
|
|
40
42
|
_DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart"
|
|
41
43
|
_PLUGIN_SPEC = "claude-smart@reflexioai"
|
|
42
44
|
_CODEX_MARKETPLACE_NAME = "reflexioai"
|
|
@@ -70,6 +72,16 @@ _BACKEND_SCRIPT = _SCRIPTS_DIR / "backend-service.sh"
|
|
|
70
72
|
_DASHBOARD_SCRIPT = _SCRIPTS_DIR / "dashboard-service.sh"
|
|
71
73
|
_REFLEXIO_DIR = Path.home() / ".reflexio"
|
|
72
74
|
_STATE_DIR = Path.home() / ".claude-smart"
|
|
75
|
+
_LOCAL_DATA_NOTICE = (
|
|
76
|
+
"Local data was kept so reinstalling claude-smart can reuse your learned "
|
|
77
|
+
"rules, sessions, logs, and local Reflexio data.\n"
|
|
78
|
+
"Kept folders:\n"
|
|
79
|
+
" ~/.claude-smart\n"
|
|
80
|
+
" ~/.reflexio\n"
|
|
81
|
+
"Delete them only if you want a full reset or need to remove local "
|
|
82
|
+
"claude-smart data from this machine:\n"
|
|
83
|
+
" rm -rf ~/.claude-smart ~/.reflexio\n"
|
|
84
|
+
)
|
|
73
85
|
_INSTALL_FAILURE_MARKER = _STATE_DIR / "install-failed"
|
|
74
86
|
_DEFAULT_STORAGE_ROOT = _REFLEXIO_DIR / "data"
|
|
75
87
|
_REFLEXIO_CONFIG_PATH = _REFLEXIO_DIR / "configs" / "config_self-host-org.json"
|
|
@@ -127,9 +139,58 @@ def _seed_reflexio_env() -> list[str]:
|
|
|
127
139
|
prefix = "" if not existing or existing.endswith("\n") else "\n"
|
|
128
140
|
with _REFLEXIO_ENV_PATH.open("a") as fh:
|
|
129
141
|
fh.write(prefix + "\n".join(f"{f}=1" for f in missing) + "\n")
|
|
142
|
+
_REFLEXIO_ENV_PATH.chmod(0o600)
|
|
130
143
|
return missing
|
|
131
144
|
|
|
132
145
|
|
|
146
|
+
def _seed_managed_reflexio_env(*, api_key: str, reflexio_url: str) -> list[str]:
|
|
147
|
+
"""Write managed Reflexio connection settings to ``~/.reflexio/.env``."""
|
|
148
|
+
user_id = _read_reflexio_env_value(env_config.REFLEXIO_USER_ID_ENV) or str(
|
|
149
|
+
uuid.uuid4()
|
|
150
|
+
)
|
|
151
|
+
return env_config.set_env_vars(
|
|
152
|
+
_REFLEXIO_ENV_PATH,
|
|
153
|
+
{
|
|
154
|
+
env_config.REFLEXIO_URL_ENV: reflexio_url,
|
|
155
|
+
env_config.REFLEXIO_API_KEY_ENV: api_key,
|
|
156
|
+
env_config.REFLEXIO_USER_ID_ENV: user_id,
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _read_reflexio_env_value(key: str) -> str:
|
|
162
|
+
try:
|
|
163
|
+
text = _REFLEXIO_ENV_PATH.read_text()
|
|
164
|
+
except OSError:
|
|
165
|
+
return ""
|
|
166
|
+
for line in text.splitlines():
|
|
167
|
+
parsed = env_config.parse_env_line(line)
|
|
168
|
+
if parsed is None:
|
|
169
|
+
continue
|
|
170
|
+
parsed_key, value = parsed
|
|
171
|
+
if parsed_key == key:
|
|
172
|
+
return value
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _semver_tuple(path: Path) -> tuple[int, int, int] | None:
|
|
177
|
+
parts = path.name.split(".", 2)
|
|
178
|
+
if len(parts) != 3:
|
|
179
|
+
return None
|
|
180
|
+
try:
|
|
181
|
+
patch = parts[2].split("-", 1)[0].split("+", 1)[0]
|
|
182
|
+
return (int(parts[0]), int(parts[1]), int(patch))
|
|
183
|
+
except ValueError:
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _installed_plugin_sort_key(path: Path) -> tuple[int, int, int, int, float]:
|
|
188
|
+
version = _semver_tuple(path)
|
|
189
|
+
if version is not None:
|
|
190
|
+
return (1, *version, path.stat().st_mtime)
|
|
191
|
+
return (0, 0, 0, 0, path.stat().st_mtime)
|
|
192
|
+
|
|
193
|
+
|
|
133
194
|
def _find_claude_code_plugin_root() -> Path | None:
|
|
134
195
|
"""Locate the installed Claude Code plugin root after native install."""
|
|
135
196
|
cache_root = (
|
|
@@ -149,7 +210,7 @@ def _find_claude_code_plugin_root() -> Path | None:
|
|
|
149
210
|
and (child / "scripts" / "smart-install.sh").is_file()
|
|
150
211
|
):
|
|
151
212
|
candidates.append(child)
|
|
152
|
-
candidates.sort(key=
|
|
213
|
+
candidates.sort(key=_installed_plugin_sort_key, reverse=True)
|
|
153
214
|
candidates.extend(
|
|
154
215
|
[
|
|
155
216
|
Path.home()
|
|
@@ -162,10 +223,9 @@ def _find_claude_code_plugin_root() -> Path | None:
|
|
|
162
223
|
]
|
|
163
224
|
)
|
|
164
225
|
for candidate in candidates:
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
):
|
|
226
|
+
if (candidate / "pyproject.toml").is_file() and (
|
|
227
|
+
candidate / "scripts" / "smart-install.sh"
|
|
228
|
+
).is_file():
|
|
169
229
|
return candidate
|
|
170
230
|
return None
|
|
171
231
|
|
|
@@ -735,7 +795,26 @@ def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
|
|
|
735
795
|
return False, output or "Codex CLI does not expose plugin marketplace commands"
|
|
736
796
|
|
|
737
797
|
|
|
738
|
-
def
|
|
798
|
+
def _configure_reflexio_setup(args: argparse.Namespace) -> None:
|
|
799
|
+
api_key = (getattr(args, "api_key", "") or "").strip()
|
|
800
|
+
if api_key:
|
|
801
|
+
reflexio_url = (
|
|
802
|
+
getattr(args, "reflexio_url", "") or _MANAGED_REFLEXIO_URL
|
|
803
|
+
).strip()
|
|
804
|
+
added = _seed_managed_reflexio_env(api_key=api_key, reflexio_url=reflexio_url)
|
|
805
|
+
changed = ", ".join(added) if added else "managed Reflexio settings"
|
|
806
|
+
sys.stdout.write(
|
|
807
|
+
f"Configured {_REFLEXIO_ENV_PATH} for managed Reflexio "
|
|
808
|
+
f"({changed}; API key {env_config.mask_secret(api_key)}).\n"
|
|
809
|
+
)
|
|
810
|
+
return
|
|
811
|
+
|
|
812
|
+
added = _seed_reflexio_env()
|
|
813
|
+
if added:
|
|
814
|
+
sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def cmd_install_codex(args: argparse.Namespace) -> int:
|
|
739
818
|
"""Install the claude-smart plugin marketplace for Codex.
|
|
740
819
|
|
|
741
820
|
Only supported from a source checkout — the wheel ships the Python
|
|
@@ -756,6 +835,7 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
|
|
|
756
835
|
"error: 'uv' not found on PATH. Install uv or restart your shell.\n"
|
|
757
836
|
)
|
|
758
837
|
return 1
|
|
838
|
+
_configure_reflexio_setup(args)
|
|
759
839
|
|
|
760
840
|
missing = _missing_codex_marketplace_files(_REPO_ROOT)
|
|
761
841
|
if missing:
|
|
@@ -767,10 +847,6 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
|
|
|
767
847
|
return 1
|
|
768
848
|
marketplace_root = _prepare_codex_local_marketplace()
|
|
769
849
|
|
|
770
|
-
added = _seed_reflexio_env()
|
|
771
|
-
if added:
|
|
772
|
-
sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
|
|
773
|
-
|
|
774
850
|
hooks_ok, hooks_msg = _enable_codex_plugin_hooks()
|
|
775
851
|
if hooks_ok:
|
|
776
852
|
sys.stdout.write(f"Enabled Codex plugin hooks ({hooks_msg}).\n")
|
|
@@ -860,6 +936,7 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
860
936
|
"Install Claude Code first: https://claude.com/claude-code\n"
|
|
861
937
|
)
|
|
862
938
|
return 1
|
|
939
|
+
_configure_reflexio_setup(args)
|
|
863
940
|
|
|
864
941
|
for cmd in (
|
|
865
942
|
["claude", "plugin", "marketplace", "add", args.source],
|
|
@@ -871,10 +948,6 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
871
948
|
sys.stderr.write(f"error: {' '.join(cmd)} failed (exit {exc.returncode})\n")
|
|
872
949
|
return exc.returncode or 1
|
|
873
950
|
|
|
874
|
-
added = _seed_reflexio_env()
|
|
875
|
-
if added:
|
|
876
|
-
sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
|
|
877
|
-
|
|
878
951
|
bootstrapped, message = _bootstrap_claude_code_install()
|
|
879
952
|
if not bootstrapped:
|
|
880
953
|
sys.stderr.write(
|
|
@@ -893,7 +966,7 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
893
966
|
return 0
|
|
894
967
|
|
|
895
968
|
|
|
896
|
-
def cmd_update(
|
|
969
|
+
def cmd_update(args: argparse.Namespace) -> int:
|
|
897
970
|
"""Update claude-smart to the latest version via the native plugin CLI.
|
|
898
971
|
|
|
899
972
|
Runs ``claude plugin update claude-smart@reflexioai``.
|
|
@@ -919,6 +992,8 @@ def cmd_update(_args: argparse.Namespace) -> int:
|
|
|
919
992
|
return exc.returncode or 1
|
|
920
993
|
|
|
921
994
|
sys.stdout.write("\nclaude-smart updated. Restart Claude Code to apply.\n")
|
|
995
|
+
if getattr(args, "api_key", ""):
|
|
996
|
+
_configure_reflexio_setup(args)
|
|
922
997
|
return 0
|
|
923
998
|
|
|
924
999
|
|
|
@@ -953,8 +1028,7 @@ def cmd_uninstall(_args: argparse.Namespace) -> int:
|
|
|
953
1028
|
|
|
954
1029
|
sys.stdout.write(
|
|
955
1030
|
"\nclaude-smart uninstalled. Restart Claude Code to apply.\n"
|
|
956
|
-
"
|
|
957
|
-
"remove manually if desired.\n"
|
|
1031
|
+
f"{_LOCAL_DATA_NOTICE}"
|
|
958
1032
|
)
|
|
959
1033
|
return 0
|
|
960
1034
|
|
|
@@ -991,8 +1065,8 @@ def cmd_uninstall_codex(_args: argparse.Namespace) -> int:
|
|
|
991
1065
|
sys.stderr.write(f"warning: could not update {_CODEX_CONFIG_PATH}\n")
|
|
992
1066
|
sys.stdout.write(
|
|
993
1067
|
"claude-smart Codex plugin and marketplace state removed. Restart Codex to apply. "
|
|
994
|
-
"Codex's global hook feature flags
|
|
995
|
-
"
|
|
1068
|
+
"Codex's global hook feature flags were left in place.\n"
|
|
1069
|
+
f"{_LOCAL_DATA_NOTICE}"
|
|
996
1070
|
)
|
|
997
1071
|
return 0
|
|
998
1072
|
|
|
@@ -1026,6 +1100,13 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|
|
1026
1100
|
agent_playbooks=agent_playbooks,
|
|
1027
1101
|
profiles=profiles,
|
|
1028
1102
|
)
|
|
1103
|
+
if not md and adapter.read_errors:
|
|
1104
|
+
sys.stdout.write(
|
|
1105
|
+
"Could not read skills or preferences from Reflexio for "
|
|
1106
|
+
f"project `{project_id}` at `{adapter.url}`.\n"
|
|
1107
|
+
f"First error: {adapter.read_errors[0]}\n"
|
|
1108
|
+
)
|
|
1109
|
+
return 1
|
|
1029
1110
|
sys.stdout.write(
|
|
1030
1111
|
md or f"_No skills or preferences yet for project `{project_id}`._\n"
|
|
1031
1112
|
)
|
|
@@ -1035,8 +1116,10 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|
|
1035
1116
|
def cmd_learn(args: argparse.Namespace) -> int:
|
|
1036
1117
|
"""Force reflexio extraction over the active session's interactions.
|
|
1037
1118
|
|
|
1038
|
-
Publishes unpublished interactions with ``force_extraction=True``
|
|
1039
|
-
|
|
1119
|
+
Publishes unpublished interactions with ``force_extraction=True`` and
|
|
1120
|
+
``override_learning_stall=True`` as an explicit retry request rather than
|
|
1121
|
+
waiting for the next batch interval. The publish call itself remains
|
|
1122
|
+
non-blocking: ``Adapter.publish`` sends ``wait_for_response=False``.
|
|
1040
1123
|
When ``args.note`` is provided, the note is appended to the session
|
|
1041
1124
|
buffer as a neutral User turn before publishing — letting the user
|
|
1042
1125
|
capture an explicit insight, preference, or workflow note alongside
|
|
@@ -1074,6 +1157,7 @@ def cmd_learn(args: argparse.Namespace) -> int:
|
|
|
1074
1157
|
session_id=session_id,
|
|
1075
1158
|
project_id=project_id,
|
|
1076
1159
|
force_extraction=True,
|
|
1160
|
+
override_learning_stall=True,
|
|
1077
1161
|
skip_aggregation=False,
|
|
1078
1162
|
)
|
|
1079
1163
|
if status == "ok":
|
|
@@ -1573,9 +1657,15 @@ def _resolve_reflexio_url() -> str:
|
|
|
1573
1657
|
str: The ``REFLEXIO_URL`` env var if set, otherwise the default
|
|
1574
1658
|
local backend endpoint.
|
|
1575
1659
|
"""
|
|
1660
|
+
env_config.load_reflexio_env()
|
|
1576
1661
|
return os.environ.get("REFLEXIO_URL", "http://localhost:8071/")
|
|
1577
1662
|
|
|
1578
1663
|
|
|
1664
|
+
def _resolve_reflexio_api_key() -> str:
|
|
1665
|
+
env_config.load_reflexio_env()
|
|
1666
|
+
return os.environ.get("REFLEXIO_API_KEY", "")
|
|
1667
|
+
|
|
1668
|
+
|
|
1579
1669
|
def _format_deleted_counts(deleted_counts: object) -> str:
|
|
1580
1670
|
"""Render ``deleted_counts`` from a ``clear_user_data`` response as a summary.
|
|
1581
1671
|
|
|
@@ -1644,7 +1734,10 @@ def cmd_clear_user(args: argparse.Namespace) -> int:
|
|
|
1644
1734
|
return 1
|
|
1645
1735
|
|
|
1646
1736
|
try:
|
|
1647
|
-
client = ReflexioClient(
|
|
1737
|
+
client = ReflexioClient(
|
|
1738
|
+
url_endpoint=_resolve_reflexio_url(),
|
|
1739
|
+
api_key=_resolve_reflexio_api_key(),
|
|
1740
|
+
)
|
|
1648
1741
|
# The companion reflexio PR adds ``clear_user_data``; keep the
|
|
1649
1742
|
# CLI buildable until both sides ship together.
|
|
1650
1743
|
response = client.clear_user_data(user_id) # pyright: ignore[reportAttributeAccessIssue]
|
|
@@ -1676,9 +1769,29 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1676
1769
|
default=_DEFAULT_MARKETPLACE_SOURCE,
|
|
1677
1770
|
help="Marketplace ref — GitHub owner/repo, or a local directory path",
|
|
1678
1771
|
)
|
|
1772
|
+
inst.add_argument(
|
|
1773
|
+
"--api-key",
|
|
1774
|
+
default="",
|
|
1775
|
+
help="Configure managed Reflexio with this API key instead of local mode",
|
|
1776
|
+
)
|
|
1777
|
+
inst.add_argument(
|
|
1778
|
+
"--reflexio-url",
|
|
1779
|
+
default=_MANAGED_REFLEXIO_URL,
|
|
1780
|
+
help="Managed Reflexio URL used only when --api-key is provided",
|
|
1781
|
+
)
|
|
1679
1782
|
inst.set_defaults(func=cmd_install)
|
|
1680
1783
|
|
|
1681
1784
|
upd = sub.add_parser("update", help="Update claude-smart to the latest version")
|
|
1785
|
+
upd.add_argument(
|
|
1786
|
+
"--api-key",
|
|
1787
|
+
default="",
|
|
1788
|
+
help="Configure managed Reflexio with this API key after updating",
|
|
1789
|
+
)
|
|
1790
|
+
upd.add_argument(
|
|
1791
|
+
"--reflexio-url",
|
|
1792
|
+
default=_MANAGED_REFLEXIO_URL,
|
|
1793
|
+
help="Managed Reflexio URL used only when --api-key is provided",
|
|
1794
|
+
)
|
|
1682
1795
|
upd.set_defaults(func=cmd_update)
|
|
1683
1796
|
|
|
1684
1797
|
uni = sub.add_parser("uninstall", help="Remove claude-smart")
|
|
@@ -1767,6 +1880,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1767
1880
|
|
|
1768
1881
|
|
|
1769
1882
|
def main(argv: list[str] | None = None) -> int:
|
|
1883
|
+
env_config.load_reflexio_env()
|
|
1770
1884
|
parser = _build_parser()
|
|
1771
1885
|
args = parser.parse_args(argv)
|
|
1772
1886
|
return args.func(args)
|
|
@@ -2,9 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import quote, urlparse
|
|
6
9
|
|
|
7
|
-
from claude_smart import cs_cite
|
|
10
|
+
from claude_smart import cs_cite, env_config
|
|
11
|
+
|
|
12
|
+
_DASHBOARD_URL_ENV = "CLAUDE_SMART_DASHBOARD_URL"
|
|
13
|
+
_CITATION_LINK_STYLE_ENV = "CLAUDE_SMART_CITATION_LINK_STYLE"
|
|
14
|
+
_DEFAULT_DASHBOARD_URL = "http://localhost:3001"
|
|
15
|
+
_REFLEXIO_URL_ENV = "REFLEXIO_URL"
|
|
16
|
+
_LOCAL_REFLEXIO_HOSTS = {"localhost", "127.0.0.1", "0.0.0.0", "::1"}
|
|
8
17
|
|
|
9
18
|
|
|
10
19
|
def _first_nonempty(*values: Any) -> str:
|
|
@@ -93,7 +102,17 @@ def render_with_registry(
|
|
|
93
102
|
if profile_lines:
|
|
94
103
|
sections.append("### Project preferences")
|
|
95
104
|
sections.extend(profile_lines)
|
|
96
|
-
|
|
105
|
+
instruction = _citation_instruction(
|
|
106
|
+
os.environ.get("CLAUDE_SMART_CITATIONS", "on"),
|
|
107
|
+
os.environ.get(_CITATION_LINK_STYLE_ENV, "markdown"),
|
|
108
|
+
)
|
|
109
|
+
if instruction:
|
|
110
|
+
sections.append(instruction)
|
|
111
|
+
exact_osc8_marker = _exact_osc8_marker_instruction(
|
|
112
|
+
playbook_entries + profile_entries
|
|
113
|
+
)
|
|
114
|
+
if exact_osc8_marker:
|
|
115
|
+
sections.append(exact_osc8_marker)
|
|
97
116
|
return "\n".join(sections) + "\n", playbook_entries + profile_entries
|
|
98
117
|
|
|
99
118
|
|
|
@@ -164,16 +183,159 @@ def render_inline_with_registry(
|
|
|
164
183
|
if profile_lines:
|
|
165
184
|
sections.append("### Relevant project preferences")
|
|
166
185
|
sections.extend(profile_lines)
|
|
167
|
-
|
|
186
|
+
instruction = _citation_instruction(
|
|
187
|
+
os.environ.get("CLAUDE_SMART_CITATIONS", "on"),
|
|
188
|
+
os.environ.get(_CITATION_LINK_STYLE_ENV, "markdown"),
|
|
189
|
+
)
|
|
190
|
+
if instruction:
|
|
191
|
+
sections.append(instruction)
|
|
192
|
+
exact_osc8_marker = _exact_osc8_marker_instruction(
|
|
193
|
+
playbook_entries + profile_entries
|
|
194
|
+
)
|
|
195
|
+
if exact_osc8_marker:
|
|
196
|
+
sections.append(exact_osc8_marker)
|
|
168
197
|
return "\n".join(sections) + "\n", playbook_entries + profile_entries
|
|
169
198
|
|
|
170
199
|
|
|
200
|
+
def render_inline_compact_with_registry(
|
|
201
|
+
*,
|
|
202
|
+
project_id: str,
|
|
203
|
+
user_playbooks: Iterable[Any],
|
|
204
|
+
agent_playbooks: Iterable[Any],
|
|
205
|
+
profiles: Iterable[Any],
|
|
206
|
+
) -> tuple[str, list[dict[str, Any]]]:
|
|
207
|
+
"""Render mid-session context as one compact logical line.
|
|
208
|
+
|
|
209
|
+
Codex currently displays ``UserPromptSubmit.additionalContext`` in the TUI.
|
|
210
|
+
This renderer preserves the model-visible ids and rule URLs needed for
|
|
211
|
+
citation tracking while avoiding the multi-line markdown block used by
|
|
212
|
+
hosts that can hide hook context.
|
|
213
|
+
"""
|
|
214
|
+
del project_id # kept for symmetry with ``render_inline_with_registry``.
|
|
215
|
+
_, playbook_entries = _format_combined_playbooks(
|
|
216
|
+
agent_playbooks=agent_playbooks, user_playbooks=user_playbooks
|
|
217
|
+
)
|
|
218
|
+
_, profile_entries = _format_profiles(profiles)
|
|
219
|
+
entries = playbook_entries + profile_entries
|
|
220
|
+
if not entries:
|
|
221
|
+
return "", []
|
|
222
|
+
|
|
223
|
+
skill_parts = []
|
|
224
|
+
preference_parts = []
|
|
225
|
+
marker_parts = []
|
|
226
|
+
link_style = os.environ.get(_CITATION_LINK_STYLE_ENV, "markdown")
|
|
227
|
+
for entry in entries:
|
|
228
|
+
title = _one_line(str(entry.get("title") or entry["content"]))
|
|
229
|
+
content = _one_line(str(entry["content"]))
|
|
230
|
+
rule_url = str(entry.get("rule_url") or "")
|
|
231
|
+
if link_style == "osc8" and rule_url:
|
|
232
|
+
linked_title = _osc8_link(
|
|
233
|
+
rule_url, _strip_trailing_sentence_punctuation(title)
|
|
234
|
+
)
|
|
235
|
+
item = _osc8_link(rule_url, _strip_trailing_sentence_punctuation(content))
|
|
236
|
+
marker_parts.append(linked_title)
|
|
237
|
+
else:
|
|
238
|
+
item = f"{content} (title: {title}"
|
|
239
|
+
if rule_url:
|
|
240
|
+
item += f"; open: {rule_url}"
|
|
241
|
+
item += ")"
|
|
242
|
+
if entry.get("kind") == "profile":
|
|
243
|
+
preference_parts.append(item)
|
|
244
|
+
else:
|
|
245
|
+
skill_parts.append(item)
|
|
246
|
+
|
|
247
|
+
memory_sections = []
|
|
248
|
+
if skill_parts:
|
|
249
|
+
label = "Skill" if len(skill_parts) == 1 else "Skills"
|
|
250
|
+
memory_sections.append(f"{label}: {'; '.join(skill_parts)}")
|
|
251
|
+
if preference_parts:
|
|
252
|
+
label = "Preference" if len(preference_parts) == 1 else "Preferences"
|
|
253
|
+
memory_sections.append(f"{label}: {'; '.join(preference_parts)}")
|
|
254
|
+
sections = [f"claude-smart: using relevant memory. {'. '.join(memory_sections)}."]
|
|
255
|
+
instruction = _compact_citation_instruction(marker_parts)
|
|
256
|
+
if instruction:
|
|
257
|
+
sections.append(instruction)
|
|
258
|
+
return " ".join(sections) + "\n", entries
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _compact_citation_instruction(marker_parts: list[str] | None = None) -> str:
|
|
262
|
+
if os.environ.get("CLAUDE_SMART_CITATIONS", "on") == "off":
|
|
263
|
+
return ""
|
|
264
|
+
link_style = os.environ.get(_CITATION_LINK_STYLE_ENV, "markdown")
|
|
265
|
+
if link_style == "osc8" and marker_parts:
|
|
266
|
+
marker = f"✨ claude-smart rule applied: {' | '.join(marker_parts)}"
|
|
267
|
+
separator_instruction = (
|
|
268
|
+
" Separate multiple linked memories with the visible ` | ` separator."
|
|
269
|
+
if len(marker_parts) > 1
|
|
270
|
+
else ""
|
|
271
|
+
)
|
|
272
|
+
return _remoteize_citation_instruction(
|
|
273
|
+
f"If used, copy this final marker exactly, preserving its hidden "
|
|
274
|
+
f"OSC 8 terminal link: `{marker}`.{separator_instruction} "
|
|
275
|
+
f"Skip when unrelated."
|
|
276
|
+
)
|
|
277
|
+
if link_style == "osc8":
|
|
278
|
+
return _remoteize_citation_instruction(
|
|
279
|
+
"If used, end with `✨ claude-smart rule applied:` followed by "
|
|
280
|
+
"the same linked memory text; keep the link, but do not show the "
|
|
281
|
+
"URL. Skip when unrelated."
|
|
282
|
+
)
|
|
283
|
+
return _remoteize_citation_instruction(
|
|
284
|
+
"Only if a listed [cs:...] item materially changes your answer, end "
|
|
285
|
+
"with one final marker like `✨ claude-smart rule applied: "
|
|
286
|
+
"[verify process state](http://localhost:3001/rules/s1-123)` using "
|
|
287
|
+
"the shown rule URL; skip the marker when unrelated."
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _citation_instruction(mode: str, link_style: str) -> str:
|
|
292
|
+
return _remoteize_citation_instruction(
|
|
293
|
+
cs_cite.citation_instruction(mode, link_style)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _remoteize_citation_instruction(text: str) -> str:
|
|
298
|
+
playbooks_url = _remote_reflexio_page_url("playbook")
|
|
299
|
+
profiles_url = _remote_reflexio_page_url("profile")
|
|
300
|
+
if not text or not playbooks_url or not profiles_url:
|
|
301
|
+
return text
|
|
302
|
+
return text.replace("http://localhost:3001/rules/s1-123", playbooks_url).replace(
|
|
303
|
+
"http://localhost:3001/rules/p1-pref", profiles_url
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _exact_osc8_marker_instruction(entries: list[dict[str, Any]]) -> str:
|
|
308
|
+
if os.environ.get("CLAUDE_SMART_CITATIONS", "on") == "off":
|
|
309
|
+
return ""
|
|
310
|
+
if os.environ.get(_CITATION_LINK_STYLE_ENV, "markdown") != "osc8":
|
|
311
|
+
return ""
|
|
312
|
+
|
|
313
|
+
marker_parts = []
|
|
314
|
+
for entry in entries:
|
|
315
|
+
rule_url = str(entry.get("rule_url") or "")
|
|
316
|
+
if not rule_url:
|
|
317
|
+
continue
|
|
318
|
+
title = _one_line(str(entry.get("title") or entry["content"]))
|
|
319
|
+
marker_parts.append(
|
|
320
|
+
_osc8_link(rule_url, _strip_trailing_sentence_punctuation(title))
|
|
321
|
+
)
|
|
322
|
+
if not marker_parts:
|
|
323
|
+
return ""
|
|
324
|
+
|
|
325
|
+
marker = f"✨ claude-smart rule applied: {' | '.join(marker_parts)}"
|
|
326
|
+
return (
|
|
327
|
+
"If any listed memory above was used, copy this exact final marker, "
|
|
328
|
+
f"preserving its hidden OSC 8 terminal links: `{marker}`. "
|
|
329
|
+
"Do not rename, summarize, or regroup the linked titles."
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
171
333
|
def _format_combined_playbooks(
|
|
172
334
|
*,
|
|
173
335
|
agent_playbooks: Iterable[Any],
|
|
174
336
|
user_playbooks: Iterable[Any],
|
|
175
337
|
) -> tuple[list[str], list[dict[str, Any]]]:
|
|
176
|
-
"""Render agent
|
|
338
|
+
"""Render agent then user playbooks with one shared rank counter."""
|
|
177
339
|
lines: list[str] = []
|
|
178
340
|
entries: list[dict[str, Any]] = []
|
|
179
341
|
rank = 0
|
|
@@ -205,11 +367,15 @@ def _append_playbook_bullet(
|
|
|
205
367
|
real_id = _field(pb, id_field)
|
|
206
368
|
item_id = cs_cite.rank_id("playbook", rank, real_id)
|
|
207
369
|
title = _title_from_content(content)
|
|
370
|
+
dashboard_url = _dashboard_url("playbook", real_id, source_kind)
|
|
371
|
+
rule_url = _rule_url(item_id, "playbook")
|
|
208
372
|
bullet = f"- [cs:{item_id}] {content}"
|
|
209
373
|
if trigger:
|
|
210
374
|
bullet += f" _(when: {trigger})_"
|
|
211
375
|
if rationale:
|
|
212
376
|
bullet += f" — *why:* {rationale}"
|
|
377
|
+
if rule_url:
|
|
378
|
+
bullet += f" _(open: {rule_url})_"
|
|
213
379
|
lines.append(bullet)
|
|
214
380
|
entries.append(
|
|
215
381
|
{
|
|
@@ -219,6 +385,8 @@ def _append_playbook_bullet(
|
|
|
219
385
|
"content": content,
|
|
220
386
|
"real_id": str(real_id) if real_id is not None else None,
|
|
221
387
|
"source_kind": source_kind,
|
|
388
|
+
"dashboard_url": dashboard_url,
|
|
389
|
+
"rule_url": rule_url,
|
|
222
390
|
}
|
|
223
391
|
)
|
|
224
392
|
return rank
|
|
@@ -238,7 +406,12 @@ def _format_profiles(
|
|
|
238
406
|
real_id = _field(p, "profile_id")
|
|
239
407
|
item_id = cs_cite.rank_id("profile", rank, real_id)
|
|
240
408
|
title = _title_from_content(content)
|
|
241
|
-
|
|
409
|
+
dashboard_url = _dashboard_url("profile", real_id)
|
|
410
|
+
rule_url = _rule_url(item_id, "profile")
|
|
411
|
+
bullet = f"- [cs:{item_id}] {content}"
|
|
412
|
+
if rule_url:
|
|
413
|
+
bullet += f" _(open: {rule_url})_"
|
|
414
|
+
lines.append(bullet)
|
|
242
415
|
entries.append(
|
|
243
416
|
{
|
|
244
417
|
"id": item_id,
|
|
@@ -246,11 +419,64 @@ def _format_profiles(
|
|
|
246
419
|
"title": title,
|
|
247
420
|
"content": content,
|
|
248
421
|
"real_id": str(real_id) if real_id is not None else None,
|
|
422
|
+
"dashboard_url": dashboard_url,
|
|
423
|
+
"rule_url": rule_url,
|
|
249
424
|
}
|
|
250
425
|
)
|
|
251
426
|
return lines, entries
|
|
252
427
|
|
|
253
428
|
|
|
429
|
+
def _dashboard_url(kind: str, real_id: Any, source_kind: str | None = None) -> str:
|
|
430
|
+
remote_url = _remote_reflexio_page_url(kind)
|
|
431
|
+
if remote_url:
|
|
432
|
+
return remote_url
|
|
433
|
+
if real_id is None:
|
|
434
|
+
return ""
|
|
435
|
+
encoded_id = quote(str(real_id), safe="")
|
|
436
|
+
base = os.environ.get(_DASHBOARD_URL_ENV, _DEFAULT_DASHBOARD_URL).rstrip("/")
|
|
437
|
+
if kind == "profile":
|
|
438
|
+
return f"{base}/preferences/project/{encoded_id}"
|
|
439
|
+
if kind == "playbook":
|
|
440
|
+
skill_kind = "shared" if source_kind == "agent_playbook" else "project"
|
|
441
|
+
return f"{base}/skills/{skill_kind}/{encoded_id}"
|
|
442
|
+
return ""
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _rule_url(item_id: str, kind: str) -> str:
|
|
446
|
+
remote_url = _remote_reflexio_page_url(kind)
|
|
447
|
+
if remote_url:
|
|
448
|
+
return remote_url
|
|
449
|
+
if not item_id:
|
|
450
|
+
return ""
|
|
451
|
+
encoded_id = quote(item_id, safe="")
|
|
452
|
+
base = os.environ.get(_DASHBOARD_URL_ENV, _DEFAULT_DASHBOARD_URL).rstrip("/")
|
|
453
|
+
return f"{base}/rules/{encoded_id}"
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _remote_reflexio_page_url(kind: str) -> str:
|
|
457
|
+
origin = _remote_reflexio_origin()
|
|
458
|
+
if not origin:
|
|
459
|
+
return ""
|
|
460
|
+
if kind == "profile":
|
|
461
|
+
return f"{origin}/profiles"
|
|
462
|
+
if kind == "playbook":
|
|
463
|
+
return f"{origin}/playbooks"
|
|
464
|
+
return ""
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _remote_reflexio_origin() -> str:
|
|
468
|
+
env_config.load_reflexio_env()
|
|
469
|
+
raw = os.environ.get(_REFLEXIO_URL_ENV, "").strip()
|
|
470
|
+
if not raw:
|
|
471
|
+
return ""
|
|
472
|
+
parsed = urlparse(raw)
|
|
473
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
474
|
+
return ""
|
|
475
|
+
if (parsed.hostname or "").lower() in _LOCAL_REFLEXIO_HOSTS:
|
|
476
|
+
return ""
|
|
477
|
+
return f"{parsed.scheme}://{parsed.netloc}".rstrip("/")
|
|
478
|
+
|
|
479
|
+
|
|
254
480
|
def _title_from_content(content: str, limit: int = 80) -> str:
|
|
255
481
|
"""Derive a compact human-readable title from a bullet's content.
|
|
256
482
|
|
|
@@ -270,6 +496,18 @@ def _title_from_content(content: str, limit: int = 80) -> str:
|
|
|
270
496
|
return text[: limit - 1].rstrip() + "…"
|
|
271
497
|
|
|
272
498
|
|
|
499
|
+
def _one_line(text: str) -> str:
|
|
500
|
+
return " ".join(text.split())
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _osc8_link(url: str, label: str) -> str:
|
|
504
|
+
return f"\x1b]8;;{url}\x1b\\{label}\x1b]8;;\x1b\\"
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _strip_trailing_sentence_punctuation(text: str) -> str:
|
|
508
|
+
return text.rstrip().rstrip(".!?")
|
|
509
|
+
|
|
510
|
+
|
|
273
511
|
def _field(obj: Any, name: str) -> Any:
|
|
274
512
|
"""Read ``name`` from either an attribute or a dict key."""
|
|
275
513
|
if isinstance(obj, dict):
|
|
@@ -17,6 +17,7 @@ turn) — see the two call sites for the small policy differences.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import json
|
|
20
|
+
import os
|
|
20
21
|
import sys
|
|
21
22
|
import time
|
|
22
23
|
|
|
@@ -59,7 +60,13 @@ def emit_context(
|
|
|
59
60
|
query=query,
|
|
60
61
|
top_k=top_k,
|
|
61
62
|
)
|
|
62
|
-
|
|
63
|
+
renderer = (
|
|
64
|
+
context_format.render_inline_compact_with_registry
|
|
65
|
+
if hook_event_name == "UserPromptSubmit"
|
|
66
|
+
and os.environ.get("CLAUDE_SMART_HOST") == "codex"
|
|
67
|
+
else context_format.render_inline_with_registry
|
|
68
|
+
)
|
|
69
|
+
markdown, registry = renderer(
|
|
63
70
|
project_id=project_id,
|
|
64
71
|
user_playbooks=user_playbooks,
|
|
65
72
|
agent_playbooks=agent_playbooks,
|