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
|
@@ -8,16 +8,26 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
|
+
import inspect
|
|
12
|
+
from collections.abc import Sequence
|
|
11
13
|
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
from typing import Any
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
14
16
|
|
|
17
|
+
from claude_smart import env_config
|
|
15
18
|
from claude_smart import runtime
|
|
16
19
|
|
|
17
20
|
_LOGGER = logging.getLogger(__name__)
|
|
18
21
|
|
|
19
22
|
_ENV_URL = "REFLEXIO_URL"
|
|
23
|
+
_ENV_API_KEY = "REFLEXIO_API_KEY"
|
|
20
24
|
_DEFAULT_URL = "http://localhost:8071/"
|
|
25
|
+
# Cap every HTTP round-trip from a hook so a hung backend can't stall the
|
|
26
|
+
# Claude Code / Codex session. reflexio's client default is 300s, which is
|
|
27
|
+
# fine for batch workloads but unacceptable on the hook path — a single
|
|
28
|
+
# unhealthy POST would freeze the user's prompt. 5s matches the precedent
|
|
29
|
+
# in ``ids.py`` for short-lived hook HTTP calls.
|
|
30
|
+
_HTTP_TIMEOUT_SECONDS = 5
|
|
21
31
|
_SEARCH_MODE_HYBRID = "hybrid" # reflexio.models.config_schema.SearchMode.HYBRID
|
|
22
32
|
_UNIFIED_ENTITY_TYPES = ("profiles", "user_playbooks", "agent_playbooks")
|
|
23
33
|
_AGENT_PLAYBOOK_APPROVAL_STATUSES = ("pending", "approved")
|
|
@@ -34,9 +44,13 @@ class Adapter:
|
|
|
34
44
|
"""
|
|
35
45
|
|
|
36
46
|
url: str = ""
|
|
47
|
+
api_key: str = ""
|
|
48
|
+
read_errors: list[str] = field(default_factory=list, init=False)
|
|
37
49
|
|
|
38
50
|
def __post_init__(self) -> None:
|
|
51
|
+
env_config.load_reflexio_env()
|
|
39
52
|
self.url = self.url or os.environ.get(_ENV_URL, _DEFAULT_URL)
|
|
53
|
+
self.api_key = self.api_key or os.environ.get(_ENV_API_KEY, "")
|
|
40
54
|
self._client: Any | None = None
|
|
41
55
|
|
|
42
56
|
# -----------------------------------------------------------------
|
|
@@ -53,7 +67,18 @@ class Adapter:
|
|
|
53
67
|
_LOGGER.debug("reflexio not importable: %s", exc)
|
|
54
68
|
return None
|
|
55
69
|
try:
|
|
56
|
-
|
|
70
|
+
try:
|
|
71
|
+
self._client = ReflexioClient(
|
|
72
|
+
url_endpoint=self.url,
|
|
73
|
+
api_key=self.api_key,
|
|
74
|
+
timeout=_HTTP_TIMEOUT_SECONDS,
|
|
75
|
+
)
|
|
76
|
+
except TypeError:
|
|
77
|
+
# Pre-0.2.x reflexio releases didn't accept ``timeout``;
|
|
78
|
+
# fall back so hooks still work against older pinned clients.
|
|
79
|
+
self._client = ReflexioClient(
|
|
80
|
+
url_endpoint=self.url, api_key=self.api_key
|
|
81
|
+
)
|
|
57
82
|
except Exception as exc: # noqa: BLE001 — adapter must never raise.
|
|
58
83
|
_LOGGER.warning("Failed to construct ReflexioClient: %s", exc)
|
|
59
84
|
return None
|
|
@@ -70,6 +95,7 @@ class Adapter:
|
|
|
70
95
|
project_id: str,
|
|
71
96
|
interactions: Sequence[dict[str, Any]],
|
|
72
97
|
force_extraction: bool = False,
|
|
98
|
+
override_learning_stall: bool = False,
|
|
73
99
|
skip_aggregation: bool = False,
|
|
74
100
|
) -> bool:
|
|
75
101
|
"""Publish buffered interactions to reflexio. Returns True on success."""
|
|
@@ -79,15 +105,22 @@ class Adapter:
|
|
|
79
105
|
if client is None:
|
|
80
106
|
return False
|
|
81
107
|
try:
|
|
82
|
-
|
|
83
|
-
user_id
|
|
84
|
-
interactions
|
|
85
|
-
agent_version
|
|
86
|
-
session_id
|
|
87
|
-
wait_for_response
|
|
88
|
-
force_extraction
|
|
89
|
-
skip_aggregation
|
|
90
|
-
|
|
108
|
+
kwargs = {
|
|
109
|
+
"user_id": project_id,
|
|
110
|
+
"interactions": list(interactions),
|
|
111
|
+
"agent_version": runtime.agent_version(),
|
|
112
|
+
"session_id": session_id,
|
|
113
|
+
"wait_for_response": False,
|
|
114
|
+
"force_extraction": force_extraction,
|
|
115
|
+
"skip_aggregation": skip_aggregation,
|
|
116
|
+
}
|
|
117
|
+
if _supports_keyword(client.publish_interaction, "override_learning_stall"):
|
|
118
|
+
kwargs["override_learning_stall"] = override_learning_stall
|
|
119
|
+
elif override_learning_stall:
|
|
120
|
+
_LOGGER.debug(
|
|
121
|
+
"publish_interaction client does not support override_learning_stall"
|
|
122
|
+
)
|
|
123
|
+
client.publish_interaction(**kwargs)
|
|
91
124
|
return True
|
|
92
125
|
except Exception as exc: # noqa: BLE001
|
|
93
126
|
_LOGGER.warning("publish_interaction failed: %s", exc)
|
|
@@ -232,13 +265,20 @@ class Adapter:
|
|
|
232
265
|
if client is None:
|
|
233
266
|
return []
|
|
234
267
|
try:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
268
|
+
if hasattr(client, "get_user_playbooks"):
|
|
269
|
+
response = client.get_user_playbooks(
|
|
270
|
+
user_id=project_id,
|
|
271
|
+
status_filter=[None], # None => CURRENT in reflexio's filter API
|
|
272
|
+
limit=top_k,
|
|
273
|
+
)
|
|
274
|
+
else:
|
|
275
|
+
response = client.search_user_playbooks(
|
|
276
|
+
user_id=project_id,
|
|
277
|
+
status_filter=[None],
|
|
278
|
+
top_k=top_k,
|
|
279
|
+
)
|
|
240
280
|
except Exception as exc: # noqa: BLE001
|
|
241
|
-
|
|
281
|
+
self._record_read_error("fetch_user_playbooks", exc)
|
|
242
282
|
return []
|
|
243
283
|
return _extract_items(response, "user_playbooks")
|
|
244
284
|
|
|
@@ -260,13 +300,20 @@ class Adapter:
|
|
|
260
300
|
if client is None:
|
|
261
301
|
return []
|
|
262
302
|
try:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
303
|
+
if hasattr(client, "get_agent_playbooks"):
|
|
304
|
+
response = client.get_agent_playbooks(
|
|
305
|
+
agent_version=runtime.agent_version(),
|
|
306
|
+
status_filter=[None],
|
|
307
|
+
limit=top_k,
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
response = client.search_agent_playbooks(
|
|
311
|
+
agent_version=runtime.agent_version(),
|
|
312
|
+
status_filter=[None],
|
|
313
|
+
top_k=top_k,
|
|
314
|
+
)
|
|
268
315
|
except Exception as exc: # noqa: BLE001
|
|
269
|
-
|
|
316
|
+
self._record_read_error("fetch_agent_playbooks", exc)
|
|
270
317
|
return []
|
|
271
318
|
return _filter_rejected_agent_playbooks(
|
|
272
319
|
_extract_items(response, "agent_playbooks")
|
|
@@ -278,13 +325,20 @@ class Adapter:
|
|
|
278
325
|
if client is None:
|
|
279
326
|
return []
|
|
280
327
|
try:
|
|
328
|
+
if hasattr(client, "get_all_profiles"):
|
|
329
|
+
response = client.get_all_profiles(limit=max(top_k * 20, 100))
|
|
330
|
+
return [
|
|
331
|
+
item
|
|
332
|
+
for item in _extract_items(response, "user_profiles")
|
|
333
|
+
if _field(item, "user_id") == project_id
|
|
334
|
+
][:top_k]
|
|
281
335
|
response = client.search_user_profiles(
|
|
282
336
|
user_id=project_id,
|
|
283
337
|
query="",
|
|
284
338
|
top_k=top_k,
|
|
285
339
|
)
|
|
286
340
|
except Exception as exc: # noqa: BLE001
|
|
287
|
-
|
|
341
|
+
self._record_read_error("fetch_project_profiles", exc)
|
|
288
342
|
return []
|
|
289
343
|
return _extract_items(response, "user_profiles")
|
|
290
344
|
|
|
@@ -329,7 +383,7 @@ class Adapter:
|
|
|
329
383
|
search_mode=_SEARCH_MODE_HYBRID,
|
|
330
384
|
)
|
|
331
385
|
except Exception as exc: # noqa: BLE001
|
|
332
|
-
|
|
386
|
+
self._record_read_error("unified search", exc)
|
|
333
387
|
return [], [], []
|
|
334
388
|
return (
|
|
335
389
|
_extract_items(response, "user_playbooks"),
|
|
@@ -374,6 +428,11 @@ class Adapter:
|
|
|
374
428
|
)
|
|
375
429
|
return up_future.result(), ap_future.result(), pr_future.result()
|
|
376
430
|
|
|
431
|
+
def _record_read_error(self, operation: str, exc: Exception) -> None:
|
|
432
|
+
message = f"{operation}: {exc}"
|
|
433
|
+
self.read_errors.append(message)
|
|
434
|
+
_LOGGER.debug("%s failed: %s", operation, exc)
|
|
435
|
+
|
|
377
436
|
|
|
378
437
|
def _extract_items(response: Any, field: str) -> list[Any]:
|
|
379
438
|
"""Pull a list field from a reflexio response object or dict, tolerating shape drift."""
|
|
@@ -386,6 +445,23 @@ def _extract_items(response: Any, field: str) -> list[Any]:
|
|
|
386
445
|
return list(value) if value else []
|
|
387
446
|
|
|
388
447
|
|
|
448
|
+
def _field(item: Any, field: str) -> Any:
|
|
449
|
+
if isinstance(item, dict):
|
|
450
|
+
return item.get(field)
|
|
451
|
+
return getattr(item, field, None)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _supports_keyword(callable_obj: Any, keyword: str) -> bool:
|
|
455
|
+
try:
|
|
456
|
+
signature = inspect.signature(callable_obj)
|
|
457
|
+
except (TypeError, ValueError):
|
|
458
|
+
return False
|
|
459
|
+
for parameter in signature.parameters.values():
|
|
460
|
+
if parameter.kind == inspect.Parameter.VAR_KEYWORD:
|
|
461
|
+
return True
|
|
462
|
+
return keyword in signature.parameters
|
|
463
|
+
|
|
464
|
+
|
|
389
465
|
def _filter_rejected_agent_playbooks(items: list[Any]) -> list[Any]:
|
|
390
466
|
"""Drop rejected shared skills defensively, even if an older backend ignores filters."""
|
|
391
467
|
return [
|