claude-smart 0.2.31 → 0.2.32
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 +172 -18
- 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 +2 -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 +20 -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 +100 -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 +86 -20
- 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 +102 -0
- package/plugin/src/claude_smart/events/stop.py +32 -6
- package/plugin/src/claude_smart/internal_call.py +30 -0
- package/plugin/src/claude_smart/publish.py +5 -0
- package/plugin/src/claude_smart/reflexio_adapter.py +102 -26
- package/plugin/uv.lock +1 -1
|
@@ -33,10 +33,11 @@ import time
|
|
|
33
33
|
from dataclasses import dataclass
|
|
34
34
|
from pathlib import Path
|
|
35
35
|
|
|
36
|
-
from claude_smart import context_format, ids, publish, state
|
|
36
|
+
from claude_smart import context_format, env_config, ids, publish, state
|
|
37
37
|
from claude_smart.reflexio_adapter import Adapter
|
|
38
38
|
|
|
39
|
-
_REFLEXIO_ENV_PATH =
|
|
39
|
+
_REFLEXIO_ENV_PATH = env_config.REFLEXIO_ENV_PATH
|
|
40
|
+
_MANAGED_REFLEXIO_URL = env_config.MANAGED_REFLEXIO_URL
|
|
40
41
|
_DEFAULT_MARKETPLACE_SOURCE = "ReflexioAI/claude-smart"
|
|
41
42
|
_PLUGIN_SPEC = "claude-smart@reflexioai"
|
|
42
43
|
_CODEX_MARKETPLACE_NAME = "reflexioai"
|
|
@@ -70,6 +71,16 @@ _BACKEND_SCRIPT = _SCRIPTS_DIR / "backend-service.sh"
|
|
|
70
71
|
_DASHBOARD_SCRIPT = _SCRIPTS_DIR / "dashboard-service.sh"
|
|
71
72
|
_REFLEXIO_DIR = Path.home() / ".reflexio"
|
|
72
73
|
_STATE_DIR = Path.home() / ".claude-smart"
|
|
74
|
+
_LOCAL_DATA_NOTICE = (
|
|
75
|
+
"Local data was kept so reinstalling claude-smart can reuse your learned "
|
|
76
|
+
"rules, sessions, logs, and local Reflexio data.\n"
|
|
77
|
+
"Kept folders:\n"
|
|
78
|
+
" ~/.claude-smart\n"
|
|
79
|
+
" ~/.reflexio\n"
|
|
80
|
+
"Delete them only if you want a full reset or need to remove local "
|
|
81
|
+
"claude-smart data from this machine:\n"
|
|
82
|
+
" rm -rf ~/.claude-smart ~/.reflexio\n"
|
|
83
|
+
)
|
|
73
84
|
_INSTALL_FAILURE_MARKER = _STATE_DIR / "install-failed"
|
|
74
85
|
_DEFAULT_STORAGE_ROOT = _REFLEXIO_DIR / "data"
|
|
75
86
|
_REFLEXIO_CONFIG_PATH = _REFLEXIO_DIR / "configs" / "config_self-host-org.json"
|
|
@@ -127,9 +138,21 @@ def _seed_reflexio_env() -> list[str]:
|
|
|
127
138
|
prefix = "" if not existing or existing.endswith("\n") else "\n"
|
|
128
139
|
with _REFLEXIO_ENV_PATH.open("a") as fh:
|
|
129
140
|
fh.write(prefix + "\n".join(f"{f}=1" for f in missing) + "\n")
|
|
141
|
+
_REFLEXIO_ENV_PATH.chmod(0o600)
|
|
130
142
|
return missing
|
|
131
143
|
|
|
132
144
|
|
|
145
|
+
def _seed_managed_reflexio_env(*, api_key: str, reflexio_url: str) -> list[str]:
|
|
146
|
+
"""Write managed Reflexio connection settings to ``~/.reflexio/.env``."""
|
|
147
|
+
return env_config.set_env_vars(
|
|
148
|
+
_REFLEXIO_ENV_PATH,
|
|
149
|
+
{
|
|
150
|
+
env_config.REFLEXIO_URL_ENV: reflexio_url,
|
|
151
|
+
env_config.REFLEXIO_API_KEY_ENV: api_key,
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
133
156
|
def _find_claude_code_plugin_root() -> Path | None:
|
|
134
157
|
"""Locate the installed Claude Code plugin root after native install."""
|
|
135
158
|
cache_root = (
|
|
@@ -162,10 +185,9 @@ def _find_claude_code_plugin_root() -> Path | None:
|
|
|
162
185
|
]
|
|
163
186
|
)
|
|
164
187
|
for candidate in candidates:
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
):
|
|
188
|
+
if (candidate / "pyproject.toml").is_file() and (
|
|
189
|
+
candidate / "scripts" / "smart-install.sh"
|
|
190
|
+
).is_file():
|
|
169
191
|
return candidate
|
|
170
192
|
return None
|
|
171
193
|
|
|
@@ -735,7 +757,26 @@ def _register_codex_marketplace(root: Path) -> tuple[bool, str]:
|
|
|
735
757
|
return False, output or "Codex CLI does not expose plugin marketplace commands"
|
|
736
758
|
|
|
737
759
|
|
|
738
|
-
def
|
|
760
|
+
def _configure_reflexio_setup(args: argparse.Namespace) -> None:
|
|
761
|
+
api_key = (getattr(args, "api_key", "") or "").strip()
|
|
762
|
+
if api_key:
|
|
763
|
+
reflexio_url = (
|
|
764
|
+
getattr(args, "reflexio_url", "") or _MANAGED_REFLEXIO_URL
|
|
765
|
+
).strip()
|
|
766
|
+
added = _seed_managed_reflexio_env(api_key=api_key, reflexio_url=reflexio_url)
|
|
767
|
+
changed = ", ".join(added) if added else "managed Reflexio settings"
|
|
768
|
+
sys.stdout.write(
|
|
769
|
+
f"Configured {_REFLEXIO_ENV_PATH} for managed Reflexio "
|
|
770
|
+
f"({changed}; API key {env_config.mask_secret(api_key)}).\n"
|
|
771
|
+
)
|
|
772
|
+
return
|
|
773
|
+
|
|
774
|
+
added = _seed_reflexio_env()
|
|
775
|
+
if added:
|
|
776
|
+
sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def cmd_install_codex(args: argparse.Namespace) -> int:
|
|
739
780
|
"""Install the claude-smart plugin marketplace for Codex.
|
|
740
781
|
|
|
741
782
|
Only supported from a source checkout — the wheel ships the Python
|
|
@@ -767,9 +808,7 @@ def cmd_install_codex(_args: argparse.Namespace) -> int:
|
|
|
767
808
|
return 1
|
|
768
809
|
marketplace_root = _prepare_codex_local_marketplace()
|
|
769
810
|
|
|
770
|
-
|
|
771
|
-
if added:
|
|
772
|
-
sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
|
|
811
|
+
_configure_reflexio_setup(args)
|
|
773
812
|
|
|
774
813
|
hooks_ok, hooks_msg = _enable_codex_plugin_hooks()
|
|
775
814
|
if hooks_ok:
|
|
@@ -871,9 +910,7 @@ def cmd_install(args: argparse.Namespace) -> int:
|
|
|
871
910
|
sys.stderr.write(f"error: {' '.join(cmd)} failed (exit {exc.returncode})\n")
|
|
872
911
|
return exc.returncode or 1
|
|
873
912
|
|
|
874
|
-
|
|
875
|
-
if added:
|
|
876
|
-
sys.stdout.write(f"Seeded {_REFLEXIO_ENV_PATH} with {', '.join(added)}.\n")
|
|
913
|
+
_configure_reflexio_setup(args)
|
|
877
914
|
|
|
878
915
|
bootstrapped, message = _bootstrap_claude_code_install()
|
|
879
916
|
if not bootstrapped:
|
|
@@ -953,8 +990,7 @@ def cmd_uninstall(_args: argparse.Namespace) -> int:
|
|
|
953
990
|
|
|
954
991
|
sys.stdout.write(
|
|
955
992
|
"\nclaude-smart uninstalled. Restart Claude Code to apply.\n"
|
|
956
|
-
"
|
|
957
|
-
"remove manually if desired.\n"
|
|
993
|
+
f"{_LOCAL_DATA_NOTICE}"
|
|
958
994
|
)
|
|
959
995
|
return 0
|
|
960
996
|
|
|
@@ -991,8 +1027,8 @@ def cmd_uninstall_codex(_args: argparse.Namespace) -> int:
|
|
|
991
1027
|
sys.stderr.write(f"warning: could not update {_CODEX_CONFIG_PATH}\n")
|
|
992
1028
|
sys.stdout.write(
|
|
993
1029
|
"claude-smart Codex plugin and marketplace state removed. Restart Codex to apply. "
|
|
994
|
-
"Codex's global hook feature flags
|
|
995
|
-
"
|
|
1030
|
+
"Codex's global hook feature flags were left in place.\n"
|
|
1031
|
+
f"{_LOCAL_DATA_NOTICE}"
|
|
996
1032
|
)
|
|
997
1033
|
return 0
|
|
998
1034
|
|
|
@@ -1026,6 +1062,13 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|
|
1026
1062
|
agent_playbooks=agent_playbooks,
|
|
1027
1063
|
profiles=profiles,
|
|
1028
1064
|
)
|
|
1065
|
+
if not md and adapter.read_errors:
|
|
1066
|
+
sys.stdout.write(
|
|
1067
|
+
"Could not read skills or preferences from Reflexio for "
|
|
1068
|
+
f"project `{project_id}` at `{adapter.url}`.\n"
|
|
1069
|
+
f"First error: {adapter.read_errors[0]}\n"
|
|
1070
|
+
)
|
|
1071
|
+
return 1
|
|
1029
1072
|
sys.stdout.write(
|
|
1030
1073
|
md or f"_No skills or preferences yet for project `{project_id}`._\n"
|
|
1031
1074
|
)
|
|
@@ -1035,8 +1078,10 @@ def cmd_show(args: argparse.Namespace) -> int:
|
|
|
1035
1078
|
def cmd_learn(args: argparse.Namespace) -> int:
|
|
1036
1079
|
"""Force reflexio extraction over the active session's interactions.
|
|
1037
1080
|
|
|
1038
|
-
Publishes unpublished interactions with ``force_extraction=True``
|
|
1039
|
-
|
|
1081
|
+
Publishes unpublished interactions with ``force_extraction=True`` and
|
|
1082
|
+
``override_learning_stall=True`` as an explicit retry request rather than
|
|
1083
|
+
waiting for the next batch interval. The publish call itself remains
|
|
1084
|
+
non-blocking: ``Adapter.publish`` sends ``wait_for_response=False``.
|
|
1040
1085
|
When ``args.note`` is provided, the note is appended to the session
|
|
1041
1086
|
buffer as a neutral User turn before publishing — letting the user
|
|
1042
1087
|
capture an explicit insight, preference, or workflow note alongside
|
|
@@ -1074,6 +1119,7 @@ def cmd_learn(args: argparse.Namespace) -> int:
|
|
|
1074
1119
|
session_id=session_id,
|
|
1075
1120
|
project_id=project_id,
|
|
1076
1121
|
force_extraction=True,
|
|
1122
|
+
override_learning_stall=True,
|
|
1077
1123
|
skip_aggregation=False,
|
|
1078
1124
|
)
|
|
1079
1125
|
if status == "ok":
|
|
@@ -1573,9 +1619,15 @@ def _resolve_reflexio_url() -> str:
|
|
|
1573
1619
|
str: The ``REFLEXIO_URL`` env var if set, otherwise the default
|
|
1574
1620
|
local backend endpoint.
|
|
1575
1621
|
"""
|
|
1622
|
+
env_config.load_reflexio_env()
|
|
1576
1623
|
return os.environ.get("REFLEXIO_URL", "http://localhost:8071/")
|
|
1577
1624
|
|
|
1578
1625
|
|
|
1626
|
+
def _resolve_reflexio_api_key() -> str:
|
|
1627
|
+
env_config.load_reflexio_env()
|
|
1628
|
+
return os.environ.get("REFLEXIO_API_KEY", "")
|
|
1629
|
+
|
|
1630
|
+
|
|
1579
1631
|
def _format_deleted_counts(deleted_counts: object) -> str:
|
|
1580
1632
|
"""Render ``deleted_counts`` from a ``clear_user_data`` response as a summary.
|
|
1581
1633
|
|
|
@@ -1644,7 +1696,10 @@ def cmd_clear_user(args: argparse.Namespace) -> int:
|
|
|
1644
1696
|
return 1
|
|
1645
1697
|
|
|
1646
1698
|
try:
|
|
1647
|
-
client = ReflexioClient(
|
|
1699
|
+
client = ReflexioClient(
|
|
1700
|
+
url_endpoint=_resolve_reflexio_url(),
|
|
1701
|
+
api_key=_resolve_reflexio_api_key(),
|
|
1702
|
+
)
|
|
1648
1703
|
# The companion reflexio PR adds ``clear_user_data``; keep the
|
|
1649
1704
|
# CLI buildable until both sides ship together.
|
|
1650
1705
|
response = client.clear_user_data(user_id) # pyright: ignore[reportAttributeAccessIssue]
|
|
@@ -1676,6 +1731,16 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1676
1731
|
default=_DEFAULT_MARKETPLACE_SOURCE,
|
|
1677
1732
|
help="Marketplace ref — GitHub owner/repo, or a local directory path",
|
|
1678
1733
|
)
|
|
1734
|
+
inst.add_argument(
|
|
1735
|
+
"--api-key",
|
|
1736
|
+
default="",
|
|
1737
|
+
help="Configure managed Reflexio with this API key instead of local mode",
|
|
1738
|
+
)
|
|
1739
|
+
inst.add_argument(
|
|
1740
|
+
"--reflexio-url",
|
|
1741
|
+
default=_MANAGED_REFLEXIO_URL,
|
|
1742
|
+
help="Managed Reflexio URL used only when --api-key is provided",
|
|
1743
|
+
)
|
|
1679
1744
|
inst.set_defaults(func=cmd_install)
|
|
1680
1745
|
|
|
1681
1746
|
upd = sub.add_parser("update", help="Update claude-smart to the latest version")
|
|
@@ -1767,6 +1832,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
1767
1832
|
|
|
1768
1833
|
|
|
1769
1834
|
def main(argv: list[str] | None = None) -> int:
|
|
1835
|
+
env_config.load_reflexio_env()
|
|
1770
1836
|
parser = _build_parser()
|
|
1771
1837
|
args = parser.parse_args(argv)
|
|
1772
1838
|
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,
|