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.
Files changed (60) hide show
  1. package/README.md +2 -2
  2. package/bin/claude-smart.js +222 -20
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/.codex-plugin/plugin.json +1 -1
  6. package/plugin/dashboard/app/api/config/route.ts +11 -2
  7. package/plugin/dashboard/app/api/health/route.ts +45 -6
  8. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +15 -7
  9. package/plugin/dashboard/app/api/rules/applied/route.ts +27 -0
  10. package/plugin/dashboard/app/configure/env/page.tsx +36 -31
  11. package/plugin/dashboard/app/configure/layout.tsx +1 -1
  12. package/plugin/dashboard/app/configure/server/page.tsx +8 -14
  13. package/plugin/dashboard/app/dashboard/page.tsx +311 -115
  14. package/plugin/dashboard/app/globals.css +80 -66
  15. package/plugin/dashboard/app/layout.tsx +13 -10
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +21 -32
  17. package/plugin/dashboard/app/preferences/page.tsx +154 -54
  18. package/plugin/dashboard/app/preferences/project/[id]/page.tsx +1 -0
  19. package/plugin/dashboard/app/rules/[id]/page.tsx +51 -0
  20. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +4 -4
  21. package/plugin/dashboard/app/sessions/page.tsx +14 -10
  22. package/plugin/dashboard/app/skills/page.tsx +175 -56
  23. package/plugin/dashboard/app/skills/project/[id]/page.tsx +22 -38
  24. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +20 -37
  25. package/plugin/dashboard/components/common/delete-learning-danger-zone.tsx +166 -0
  26. package/plugin/dashboard/components/common/empty-state.tsx +4 -2
  27. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  28. package/plugin/dashboard/components/common/page-header.tsx +5 -3
  29. package/plugin/dashboard/components/common/page-tabs.tsx +4 -4
  30. package/plugin/dashboard/components/common/stat-card.tsx +9 -5
  31. package/plugin/dashboard/components/layout/sidebar.tsx +24 -9
  32. package/plugin/dashboard/components/layout/top-bar.tsx +37 -25
  33. package/plugin/dashboard/components/ui/input.tsx +1 -0
  34. package/plugin/dashboard/hooks/use-settings.tsx +30 -61
  35. package/plugin/dashboard/hooks/use-stall-state.ts +5 -9
  36. package/plugin/dashboard/lib/config-file.ts +4 -0
  37. package/plugin/dashboard/lib/reflexio-client.ts +23 -48
  38. package/plugin/dashboard/lib/session-reader.ts +222 -6
  39. package/plugin/dashboard/lib/types.ts +21 -1
  40. package/plugin/dashboard/package-lock.json +70 -95
  41. package/plugin/dashboard/package.json +5 -2
  42. package/plugin/pyproject.toml +1 -1
  43. package/plugin/scripts/_lib.sh +61 -0
  44. package/plugin/scripts/backend-service.sh +13 -2
  45. package/plugin/scripts/cli.sh +1 -0
  46. package/plugin/scripts/codex-hook.js +101 -3
  47. package/plugin/scripts/dashboard-service.sh +95 -19
  48. package/plugin/scripts/hook_entry.sh +13 -7
  49. package/plugin/scripts/smart-install.sh +18 -13
  50. package/plugin/src/claude_smart/cli.py +138 -24
  51. package/plugin/src/claude_smart/context_format.py +244 -6
  52. package/plugin/src/claude_smart/context_inject.py +8 -1
  53. package/plugin/src/claude_smart/cs_cite.py +186 -34
  54. package/plugin/src/claude_smart/env_config.py +103 -0
  55. package/plugin/src/claude_smart/events/stop.py +32 -6
  56. package/plugin/src/claude_smart/ids.py +32 -0
  57. package/plugin/src/claude_smart/internal_call.py +30 -0
  58. package/plugin/src/claude_smart/publish.py +8 -3
  59. package/plugin/src/claude_smart/reflexio_adapter.py +106 -29
  60. 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, Sequence
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
- self._client = ReflexioClient(url_endpoint=self.url)
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
- client.publish_interaction(
83
- user_id=project_id,
84
- interactions=list(interactions),
85
- agent_version=runtime.agent_version(),
86
- session_id=session_id,
87
- wait_for_response=False,
88
- force_extraction=force_extraction,
89
- skip_aggregation=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)
@@ -217,9 +250,10 @@ class Adapter:
217
250
  def fetch_user_playbooks(self, *, project_id: str, top_k: int = 10) -> list[Any]:
218
251
  """Fetch CURRENT user playbooks for ``project_id``.
219
252
 
220
- User playbooks are project-scoped: filtering by ``user_id=project_id``
221
- mirrors the publish path (``publish_interaction(user_id=project_id, …)``)
222
- so each project only sees the playbooks it produced.
253
+ User playbooks are scoped by the resolved user id: local installs use
254
+ the project name, while managed installs use ``REFLEXIO_USER_ID``.
255
+ Filtering mirrors the publish path
256
+ (``publish_interaction(user_id=project_id, …)``).
223
257
 
224
258
  Args:
225
259
  project_id (str): reflexio ``user_id`` for this repo.
@@ -232,13 +266,20 @@ class Adapter:
232
266
  if client is None:
233
267
  return []
234
268
  try:
235
- response = client.search_user_playbooks(
236
- user_id=project_id,
237
- status_filter=[None], # None => CURRENT in reflexio's filter API
238
- top_k=top_k,
239
- )
269
+ if hasattr(client, "get_user_playbooks"):
270
+ response = client.get_user_playbooks(
271
+ user_id=project_id,
272
+ status_filter=[None], # None => CURRENT in reflexio's filter API
273
+ limit=top_k,
274
+ )
275
+ else:
276
+ response = client.search_user_playbooks(
277
+ user_id=project_id,
278
+ status_filter=[None],
279
+ top_k=top_k,
280
+ )
240
281
  except Exception as exc: # noqa: BLE001
241
- _LOGGER.debug("search_user_playbooks failed: %s", exc)
282
+ self._record_read_error("fetch_user_playbooks", exc)
242
283
  return []
243
284
  return _extract_items(response, "user_playbooks")
244
285
 
@@ -260,13 +301,20 @@ class Adapter:
260
301
  if client is None:
261
302
  return []
262
303
  try:
263
- response = client.search_agent_playbooks(
264
- agent_version=runtime.agent_version(),
265
- status_filter=[None],
266
- top_k=top_k,
267
- )
304
+ if hasattr(client, "get_agent_playbooks"):
305
+ response = client.get_agent_playbooks(
306
+ agent_version=runtime.agent_version(),
307
+ status_filter=[None],
308
+ limit=top_k,
309
+ )
310
+ else:
311
+ response = client.search_agent_playbooks(
312
+ agent_version=runtime.agent_version(),
313
+ status_filter=[None],
314
+ top_k=top_k,
315
+ )
268
316
  except Exception as exc: # noqa: BLE001
269
- _LOGGER.debug("search_agent_playbooks failed: %s", exc)
317
+ self._record_read_error("fetch_agent_playbooks", exc)
270
318
  return []
271
319
  return _filter_rejected_agent_playbooks(
272
320
  _extract_items(response, "agent_playbooks")
@@ -278,13 +326,20 @@ class Adapter:
278
326
  if client is None:
279
327
  return []
280
328
  try:
329
+ if hasattr(client, "get_all_profiles"):
330
+ response = client.get_all_profiles(limit=max(top_k * 20, 100))
331
+ return [
332
+ item
333
+ for item in _extract_items(response, "user_profiles")
334
+ if _field(item, "user_id") == project_id
335
+ ][:top_k]
281
336
  response = client.search_user_profiles(
282
337
  user_id=project_id,
283
338
  query="",
284
339
  top_k=top_k,
285
340
  )
286
341
  except Exception as exc: # noqa: BLE001
287
- _LOGGER.debug("search_user_profiles failed: %s", exc)
342
+ self._record_read_error("fetch_project_profiles", exc)
288
343
  return []
289
344
  return _extract_items(response, "user_profiles")
290
345
 
@@ -329,7 +384,7 @@ class Adapter:
329
384
  search_mode=_SEARCH_MODE_HYBRID,
330
385
  )
331
386
  except Exception as exc: # noqa: BLE001
332
- _LOGGER.debug("unified search failed: %s", exc)
387
+ self._record_read_error("unified search", exc)
333
388
  return [], [], []
334
389
  return (
335
390
  _extract_items(response, "user_playbooks"),
@@ -374,6 +429,11 @@ class Adapter:
374
429
  )
375
430
  return up_future.result(), ap_future.result(), pr_future.result()
376
431
 
432
+ def _record_read_error(self, operation: str, exc: Exception) -> None:
433
+ message = f"{operation}: {exc}"
434
+ self.read_errors.append(message)
435
+ _LOGGER.debug("%s failed: %s", operation, exc)
436
+
377
437
 
378
438
  def _extract_items(response: Any, field: str) -> list[Any]:
379
439
  """Pull a list field from a reflexio response object or dict, tolerating shape drift."""
@@ -386,6 +446,23 @@ def _extract_items(response: Any, field: str) -> list[Any]:
386
446
  return list(value) if value else []
387
447
 
388
448
 
449
+ def _field(item: Any, field: str) -> Any:
450
+ if isinstance(item, dict):
451
+ return item.get(field)
452
+ return getattr(item, field, None)
453
+
454
+
455
+ def _supports_keyword(callable_obj: Any, keyword: str) -> bool:
456
+ try:
457
+ signature = inspect.signature(callable_obj)
458
+ except (TypeError, ValueError):
459
+ return False
460
+ for parameter in signature.parameters.values():
461
+ if parameter.kind == inspect.Parameter.VAR_KEYWORD:
462
+ return True
463
+ return keyword in signature.parameters
464
+
465
+
389
466
  def _filter_rejected_agent_playbooks(items: list[Any]) -> list[Any]:
390
467
  """Drop rejected shared skills defensively, even if an older backend ignores filters."""
391
468
  return [
package/plugin/uv.lock CHANGED
@@ -419,7 +419,7 @@ wheels = [
419
419
 
420
420
  [[package]]
421
421
  name = "claude-smart"
422
- version = "0.2.31"
422
+ version = "0.2.33"
423
423
  source = { editable = "." }
424
424
  dependencies = [
425
425
  { name = "chromadb" },