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.
Files changed (59) hide show
  1. package/README.md +2 -2
  2. package/bin/claude-smart.js +172 -18
  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 +2 -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 +20 -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 +100 -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 +86 -20
  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 +102 -0
  55. package/plugin/src/claude_smart/events/stop.py +32 -6
  56. package/plugin/src/claude_smart/internal_call.py +30 -0
  57. package/plugin/src/claude_smart/publish.py +5 -0
  58. package/plugin/src/claude_smart/reflexio_adapter.py +102 -26
  59. 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)
@@ -232,13 +265,20 @@ class Adapter:
232
265
  if client is None:
233
266
  return []
234
267
  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
- )
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
- _LOGGER.debug("search_user_playbooks failed: %s", exc)
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
- response = client.search_agent_playbooks(
264
- agent_version=runtime.agent_version(),
265
- status_filter=[None],
266
- top_k=top_k,
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
- _LOGGER.debug("search_agent_playbooks failed: %s", exc)
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
- _LOGGER.debug("search_user_profiles failed: %s", exc)
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
- _LOGGER.debug("unified search failed: %s", exc)
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 [
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.32"
423
423
  source = { editable = "." }
424
424
  dependencies = [
425
425
  { name = "chromadb" },