claude-smart 0.2.46 → 0.2.47

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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/opencode/dist/server.mjs +76 -2
  10. package/plugin/opencode/server.mts +79 -2
  11. package/plugin/pyproject.toml +6 -2
  12. package/plugin/scripts/smart-install.sh +7 -1
  13. package/plugin/src/claude_smart/cli.py +210 -22
  14. package/plugin/src/claude_smart/context_format.py +9 -9
  15. package/plugin/src/claude_smart/cs_cite.py +66 -19
  16. package/plugin/uv.lock +5 -5
  17. package/plugin/vendor/reflexio/README.md +3 -3
  18. package/plugin/vendor/reflexio/pyproject.toml +1 -1
  19. package/plugin/vendor/reflexio/reflexio/README.md +3 -1
  20. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  21. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  22. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  23. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  24. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  25. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  26. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  27. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  28. package/plugin/vendor/reflexio/reflexio/lib/_search.py +23 -5
  29. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  30. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  31. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  32. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  33. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +16 -0
  34. package/plugin/vendor/reflexio/reflexio/server/README.md +14 -2
  35. package/plugin/vendor/reflexio/reflexio/server/api.py +176 -29
  36. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  37. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  38. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  39. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  40. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
  41. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  42. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  43. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -1
  44. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  45. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  46. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  47. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  48. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +45 -19
  49. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  50. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  51. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +129 -111
  52. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  53. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  54. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  55. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  56. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  57. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  58. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +14 -2
  59. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +2 -0
  60. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  61. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
  62. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +5 -3
  63. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  64. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +262 -107
  65. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  66. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
  68. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  70. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  71. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
  72. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +19 -3
  73. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  74. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
  75. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  76. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +13 -0
  77. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  85. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -269,7 +269,7 @@ def _compact_citation_instruction(marker_parts: list[str] | None = None) -> str:
269
269
  gate = cs_cite.WHEN_TO_CITE_COMPACT
270
270
  link_style = os.environ.get(_CITATION_LINK_STYLE_ENV, "markdown")
271
271
  if link_style == "osc8" and marker_parts:
272
- marker = f" claude-smart rule applied: {' | '.join(marker_parts)}"
272
+ marker = cs_cite.build_marker(" | ".join(marker_parts), "osc8")
273
273
  separator_instruction = (
274
274
  " Separate multiple linked memories with the visible ` | ` separator."
275
275
  if len(marker_parts) > 1
@@ -281,12 +281,13 @@ def _compact_citation_instruction(marker_parts: list[str] | None = None) -> str:
281
281
  )
282
282
  if link_style == "osc8":
283
283
  return _remoteize_citation_instruction(
284
- f"{gate} If you do, end with `✨ claude-smart rule applied:` "
285
- "followed by the same linked memory text; keep the link, but do "
286
- "not show the URL."
284
+ f"{gate} If you do, end with `{cs_cite.MARKER_PREFIX}` "
285
+ "followed by the same linked memory text, then "
286
+ f"`{cs_cite.marker_attribution('osc8')}` linking to the reflexio "
287
+ "repo; keep the links, but do not show the URL."
287
288
  )
288
289
  if marker_parts:
289
- marker = f" claude-smart rule applied: {' | '.join(marker_parts)}"
290
+ marker = cs_cite.build_marker(" | ".join(marker_parts), "markdown")
290
291
  separator_instruction = (
291
292
  " Separate multiple linked memories with the visible ` | ` separator."
292
293
  if len(marker_parts) > 1
@@ -297,9 +298,8 @@ def _compact_citation_instruction(marker_parts: list[str] | None = None) -> str:
297
298
  f"links: `{marker}`.{separator_instruction}"
298
299
  )
299
300
  return _remoteize_citation_instruction(
300
- f"{gate} If you do, end with one final marker like `✨ claude-smart "
301
- "rule applied: [verify process state](http://localhost:3001/rules/"
302
- "s1-123)` using the shown rule URL."
301
+ f"{gate} If you do, end with one final marker like "
302
+ f"`{cs_cite.MARKDOWN_EXAMPLE_ONE}` using the shown rule URL."
303
303
  )
304
304
 
305
305
 
@@ -337,7 +337,7 @@ def _exact_osc8_marker_instruction(entries: list[dict[str, Any]]) -> str:
337
337
  if not marker_parts:
338
338
  return ""
339
339
 
340
- marker = f" claude-smart rule applied: {' | '.join(marker_parts)}"
340
+ marker = cs_cite.build_marker(" | ".join(marker_parts), "osc8")
341
341
  return (
342
342
  "If any listed memory above was used, copy this exact final marker, "
343
343
  f"preserving its hidden OSC 8 terminal links: `{marker}`. "
@@ -85,36 +85,83 @@ WHEN_TO_CITE_COMPACT = (
85
85
 
86
86
  _COMPACT_INTRO = f"_{WHEN_TO_CITE}"
87
87
 
88
+ # Public repo the citation marker attributes its learnings to. The cue rides
89
+ # along on every emitted marker so users see that the applied learning was
90
+ # generated by Reflexio (and can star the repo).
91
+ REFLEXIO_REPO_URL = "https://github.com/ReflexioAI/reflexio"
92
+ MARKER_PREFIX = "✨ claude-smart rule applied:"
93
+ _ATTRIBUTION_LABEL = "⚡Reflexio"
94
+
95
+
96
+ def _osc8_link(url: str, text: str) -> str:
97
+ """Wrap ``text`` in an OSC 8 terminal hyperlink pointing at ``url``."""
98
+ return f"\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\"
99
+
100
+
101
+ def marker_attribution(link_style: str = "markdown") -> str:
102
+ """Return the trailing ``· ⚡Reflexio`` attribution, linked to the repo.
103
+
104
+ Args:
105
+ link_style (str): ``"markdown"`` for a markdown link, or ``"osc8"`` for
106
+ a terminal-native hyperlink. Unknown values fall back to markdown.
107
+
108
+ Returns:
109
+ str: The attribution segment, beginning with a leading separator.
110
+ """
111
+ if link_style == "osc8":
112
+ return f" · {_osc8_link(REFLEXIO_REPO_URL, _ATTRIBUTION_LABEL)}"
113
+ return f" · [{_ATTRIBUTION_LABEL}]({REFLEXIO_REPO_URL})"
114
+
115
+
116
+ def build_marker(body: str, link_style: str = "markdown") -> str:
117
+ """Build the full citation marker line, including the Reflexio attribution.
118
+
119
+ Single source of truth for the marker line so the prefix and attribution
120
+ cannot drift across the markdown / OSC 8 / compact render paths.
121
+
122
+ Args:
123
+ body (str): The linked memory title(s) — already formatted as markdown
124
+ or OSC 8 links, joined with `` | `` for multiple items.
125
+ link_style (str): ``"markdown"`` or ``"osc8"``; selects the attribution
126
+ link form.
127
+
128
+ Returns:
129
+ str: ``✨ claude-smart rule applied: <body> · ⚡Reflexio``.
130
+ """
131
+ return f"{MARKER_PREFIX} {body}{marker_attribution(link_style)}"
132
+
133
+
134
+ MARKDOWN_EXAMPLE_ONE = build_marker(
135
+ "[verify process state](http://localhost:3001/rules/s1-123)", "markdown"
136
+ )
137
+ _MARKDOWN_EXAMPLE_MULTI = build_marker(
138
+ "[git safety](http://localhost:3001/rules/s1-123) | "
139
+ "[brief answer preference](http://localhost:3001/rules/p1-pref)",
140
+ "markdown",
141
+ )
88
142
  _MARKDOWN_MARKER_PARAGRAPH = (
89
143
  "How to format: use human-readable linked titles, not raw ids. "
90
144
  "Format for one item: "
91
- "`✨ claude-smart rule applied: "
92
- "[verify process state](http://localhost:3001/rules/s1-123)`. "
145
+ f"`{MARKDOWN_EXAMPLE_ONE}`. "
93
146
  "For multiple items: "
94
- "`✨ claude-smart rule applied: "
95
- "[git safety](http://localhost:3001/rules/s1-123) | "
96
- "[brief answer preference](http://localhost:3001/rules/p1-pref)`. "
147
+ f"`{_MARKDOWN_EXAMPLE_MULTI}`. "
148
+ "Always end the marker with the ` · ⚡Reflexio` attribution shown — a link "
149
+ "to the reflexio repo — exactly as given. "
97
150
  "Separate multiple linked memories with the visible ` | ` separator. "
98
151
  "Use the dashboard URL shown beside each cited item; do not invent URLs. "
99
152
  "Do not include `[cs:…]` ids in the marker line. Never use the old "
100
153
  "`✨ 1 claude-smart learning applied [cs:...]` marker format._"
101
154
  )
102
155
 
103
- _OSC8_EXAMPLE_ONE = (
104
- "✨ claude-smart rule applied: "
105
- "\x1b]8;;http://localhost:3001/rules/s1-123\x1b\\"
106
- "verify process state"
107
- "\x1b]8;;\x1b\\"
156
+ _OSC8_EXAMPLE_ONE = build_marker(
157
+ _osc8_link("http://localhost:3001/rules/s1-123", "verify process state"),
158
+ "osc8",
108
159
  )
109
- _OSC8_EXAMPLE_MULTI = (
110
- "✨ claude-smart rule applied: "
111
- "\x1b]8;;http://localhost:3001/rules/s1-123\x1b\\"
112
- "git safety"
113
- "\x1b]8;;\x1b\\"
114
- " | "
115
- "\x1b]8;;http://localhost:3001/rules/p1-pref\x1b\\"
116
- "brief answer preference"
117
- "\x1b]8;;\x1b\\"
160
+ _OSC8_EXAMPLE_MULTI = build_marker(
161
+ _osc8_link("http://localhost:3001/rules/s1-123", "git safety")
162
+ + " | "
163
+ + _osc8_link("http://localhost:3001/rules/p1-pref", "brief answer preference"),
164
+ "osc8",
118
165
  )
119
166
  _OSC8_MARKER_PARAGRAPH = (
120
167
  "How to format: use human-readable OSC 8 terminal hyperlinks, not raw ids "
package/plugin/uv.lock CHANGED
@@ -476,7 +476,7 @@ wheels = [
476
476
 
477
477
  [[package]]
478
478
  name = "claude-smart"
479
- version = "0.2.46"
479
+ version = "0.2.47"
480
480
  source = { editable = "." }
481
481
  dependencies = [
482
482
  { name = "chromadb" },
@@ -494,7 +494,7 @@ dev = [
494
494
  requires-dist = [
495
495
  { name = "chromadb", specifier = ">=0.5" },
496
496
  { name = "einops", specifier = ">=0.8.0" },
497
- { name = "reflexio-ai", specifier = ">=0.2.25" },
497
+ { name = "reflexio-ai", specifier = ">=0.2.27" },
498
498
  { name = "sqlite-vec", specifier = ">=0.1.6" },
499
499
  ]
500
500
 
@@ -2760,7 +2760,7 @@ wheels = [
2760
2760
 
2761
2761
  [[package]]
2762
2762
  name = "reflexio-ai"
2763
- version = "0.2.25"
2763
+ version = "0.2.27"
2764
2764
  source = { registry = "https://pypi.org/simple" }
2765
2765
  dependencies = [
2766
2766
  { name = "aiohttp" },
@@ -2800,9 +2800,9 @@ dependencies = [
2800
2800
  { name = "websocket-client" },
2801
2801
  { name = "xlsxwriter" },
2802
2802
  ]
2803
- sdist = { url = "https://files.pythonhosted.org/packages/0f/da/323d788e55db2041caf1ffbb52a3536b959ac12ee8e22d40e7d53da10e96/reflexio_ai-0.2.25.tar.gz", hash = "sha256:4435145c865e397a9e4fb86560fa9378ba9094e99646f9c2df65cb4de7dd4fac", size = 754836, upload-time = "2026-06-10T18:03:11.997Z" }
2803
+ sdist = { url = "https://files.pythonhosted.org/packages/85/22/64429eb84ae74aaa6d36f0a2e0d91a27b35ee33ef17c18411d54f08706ba/reflexio_ai-0.2.27.tar.gz", hash = "sha256:e433209ca848245e957943c6476f2bba32581de8804d535d25ca0596b0da0afd", size = 1112139, upload-time = "2026-06-30T06:26:21.88Z" }
2804
2804
  wheels = [
2805
- { url = "https://files.pythonhosted.org/packages/16/7c/ef6cfaf172eecc02d6834828c549c314d013b6b3019bf7186dc50eeb9c5c/reflexio_ai-0.2.25-py3-none-any.whl", hash = "sha256:927b20323cbfe6a32df2092ce617ed2abec78a57b8c70e61eff501862b455dca", size = 1024584, upload-time = "2026-06-10T18:03:13.525Z" },
2805
+ { url = "https://files.pythonhosted.org/packages/68/22/d65332c75b9072e8838d51f68354d8d17c7636aa1cf73a20cfc1147b4364/reflexio_ai-0.2.27-py3-none-any.whl", hash = "sha256:4598f4aaa61e3c755a04573ab9711a7b5673ebf268029b2cef21cdcd5d4abfcf", size = 1461400, upload-time = "2026-06-30T06:26:19.965Z" },
2806
2806
  ]
2807
2807
 
2808
2808
  [[package]]
@@ -7,7 +7,7 @@
7
7
 
8
8
  [![Python >= 3.12](https://img.shields.io/badge/python-%3E%3D3.12-blue)](https://www.python.org/)
9
9
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green)](LICENSE)
10
- [![PyPI](https://img.shields.io/pypi/v/reflexio-client)](https://pypi.org/project/reflexio-client/)
10
+ [![PyPI](https://img.shields.io/pypi/v/reflexio-ai)](https://pypi.org/project/reflexio-ai/)
11
11
  [![Downloads](https://static.pepy.tech/badge/reflexio-ai/month)](https://pepy.tech/project/reflexio-ai)
12
12
  [![Search p50 57ms](https://img.shields.io/badge/search-57ms%20p50-brightgreen)](reflexio/benchmarks/retrieval_latency/results/report.md)
13
13
  [![GitHub stars](https://img.shields.io/github/stars/ReflexioAI/reflexio)](https://github.com/ReflexioAI/reflexio/stargazers)
@@ -244,10 +244,10 @@ Reflexio will automatically generate profiles and extract playbooks in the backg
244
244
 
245
245
  For detailed API documentation, see the [full API reference](https://www.reflexio.ai/docs/api-reference).
246
246
 
247
- Install the client:
247
+ Install the package:
248
248
 
249
249
  ```shell
250
- pip install reflexio-client
250
+ pip install reflexio-ai
251
251
  ```
252
252
 
253
253
  ### Basic usage
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "reflexio-ai"
3
- version = "0.2.26"
3
+ version = "0.2.27"
4
4
  description = "A Python library for the Reflexio"
5
5
  authors = [{name = "Reflexio Team"}]
6
6
  readme = "README.md"
@@ -102,6 +102,7 @@ Description: FastAPI backend server that processes user interactions to generate
102
102
 
103
103
  ### Main Entry Points
104
104
  - **API**: `api.py` - FastAPI routes
105
+ - **Extension Registry**: `extensions.py` - optional capability/service registration for OSS and enterprise integrations
105
106
  - **Endpoint Helpers**: `api_endpoints/` - Request handlers calling `Reflexio` (reflexio_lib)
106
107
  - **Core Service**: `services/generation_service.py` - Main orchestrator
107
108
 
@@ -141,7 +142,8 @@ client (Python SDK)
141
142
  - `playbook_optimizer/` - Scenario-based playbook optimization experiments
142
143
  - `braintrust/` - Braintrust eval export/sync support
143
144
  - `lineage/` - Resolve current records and schedule tombstone garbage collection for superseded profile/playbook rows
144
- - `storage/` - Abstract layer (SQLite prod, LocalJSON test)
145
+ - `governance/` - Subject-reference contracts and retention/barrier helpers used by storage and lineage
146
+ - `storage/` - Abstract layer (SQLite prod, LocalJSON test) with governance-aware write validation
145
147
  - `pre_retrieval/` - Query rewriting and document expansion helpers
146
148
  - `configurator/` - YAML config loader
147
149
  - **`site_var/`**: Global settings singleton
@@ -24,7 +24,7 @@ _DEFAULT_STORAGE = "sqlite"
24
24
  def default_org_id() -> str:
25
25
  """Resolve the default org id, honoring ``REFLEXIO_DEFAULT_ORG_ID``.
26
26
 
27
- Mirrors :func:`reflexio.server._auth.default_get_org_id` so the local CLI
27
+ Mirrors :func:`reflexio.server.auth.default_get_org_id` so the local CLI
28
28
  helpers (``setup``, ``config show`` / ``config local``) resolve the same
29
29
  ``config_<org>.json`` the running no-auth server reads. Uses ``os.environ``
30
30
  directly (not ``env_utils``) to keep this module's import lightweight — see
@@ -33,7 +33,7 @@ _PROVIDERS: dict[str, dict[str, str]] = {
33
33
  },
34
34
  "anthropic": {
35
35
  "env_var": "ANTHROPIC_API_KEY",
36
- "model": "claude-sonnet-4-6",
36
+ "model": "claude-sonnet-5",
37
37
  "display": "Anthropic",
38
38
  },
39
39
  "gemini": {
@@ -53,7 +53,7 @@ _PROVIDERS: dict[str, dict[str, str]] = {
53
53
  },
54
54
  "minimax": {
55
55
  "env_var": "MINIMAX_API_KEY",
56
- "model": "MiniMax-M2.7",
56
+ "model": "MiniMax-M3",
57
57
  "display": "MiniMax",
58
58
  },
59
59
  "dashscope": {
@@ -44,7 +44,7 @@ def find_pids_on_port(port: int) -> list[int]:
44
44
  """Find process IDs listening on the given port using lsof."""
45
45
  try:
46
46
  result = subprocess.run(
47
- ["lsof", "-t", f"-i:{port}"],
47
+ ["lsof", "-nP", "-t", f"-iTCP:{port}", "-sTCP:LISTEN"],
48
48
  capture_output=True,
49
49
  text=True,
50
50
  check=False,
@@ -76,6 +76,47 @@ def find_pids_by_pattern(pattern: str) -> list[int]:
76
76
  return []
77
77
 
78
78
 
79
+ def get_requested_port_conflicts(ports: dict[str, int]) -> list[str]:
80
+ """Return startup-blocking conflicts for requested service ports."""
81
+ conflicts: list[str] = []
82
+ services_by_port: dict[int, list[str]] = {}
83
+ for name, port in ports.items():
84
+ services_by_port.setdefault(port, []).append(name)
85
+
86
+ for port, names in sorted(services_by_port.items()):
87
+ if len(names) > 1:
88
+ conflicts.append(
89
+ f"port {port} is assigned to multiple services: {', '.join(sorted(names))}"
90
+ )
91
+
92
+ for name, port in sorted(ports.items()):
93
+ pids = find_pids_on_port(port)
94
+ if pids:
95
+ pid_list = ", ".join(str(pid) for pid in sorted(set(pids)))
96
+ conflicts.append(
97
+ f"{name} port {port} is already in use by PID(s): {pid_list}"
98
+ )
99
+
100
+ return conflicts
101
+
102
+
103
+ def ensure_requested_ports_available(ports: dict[str, int]) -> None:
104
+ """Exit with a clear message when requested service ports are unavailable."""
105
+ conflicts = get_requested_port_conflicts(ports)
106
+ if not conflicts:
107
+ return
108
+
109
+ sys.stdout.write("Error: cannot start Reflexio services because ports conflict.\n")
110
+ for conflict in conflicts:
111
+ sys.stdout.write(f" - {conflict}\n")
112
+ sys.stdout.write(
113
+ "Stop the existing service with `uv run reflexio services stop` or choose "
114
+ "different BACKEND_PORT/FRONTEND_PORT/DOCS_PORT values.\n"
115
+ )
116
+ sys.stdout.flush()
117
+ sys.exit(1)
118
+
119
+
79
120
  def kill_processes(
80
121
  pids: list[int], graceful_timeout: float = 2.0, force: bool = False
81
122
  ) -> None:
@@ -233,6 +274,8 @@ def run_services(
233
274
  signal.signal(signal.SIGINT, shutdown)
234
275
  signal.signal(signal.SIGTERM, shutdown)
235
276
 
277
+ ensure_requested_ports_available(ports)
278
+
236
279
  for svc in services:
237
280
  noise_env = _NOISE_SUPPRESSION_ENV.get(svc.name, {})
238
281
  merged_env = {**os.environ, **(svc.env or {}), **noise_env}
@@ -124,6 +124,7 @@ from .cache import InMemoryCache
124
124
  class _ClientConfigPayload(Config):
125
125
  model_config = ConfigDict(extra="allow")
126
126
 
127
+
127
128
  T = TypeVar("T")
128
129
 
129
130
  logger = logging.getLogger(__name__)
@@ -1153,9 +1154,13 @@ class ReflexioClient:
1153
1154
  force_refresh: bool = False,
1154
1155
  *,
1155
1156
  user_id: str | None = None,
1157
+ profile_id: str | None = None,
1158
+ query: str | None = None,
1156
1159
  start_time: datetime | None = None,
1157
1160
  end_time: datetime | None = None,
1158
1161
  top_k: int | None = None,
1162
+ source: str | None = None,
1163
+ profile_time_to_live: str | None = None,
1159
1164
  status_filter: list[Status | str | None] | None = None,
1160
1165
  tags: list[str] | None = None,
1161
1166
  ) -> GetProfilesViewResponse:
@@ -1165,9 +1170,13 @@ class ReflexioClient:
1165
1170
  request (Optional[GetUserProfilesRequest]): The list request object (alternative to kwargs)
1166
1171
  force_refresh (bool, optional): If True, bypass cache and fetch fresh data. Defaults to False.
1167
1172
  user_id (str): The user ID to get profiles for
1173
+ profile_id (Optional[str]): Exact profile ID filter.
1174
+ query (Optional[str]): Case-insensitive text filter across visible fields.
1168
1175
  start_time (Optional[datetime]): Filter by start time
1169
1176
  end_time (Optional[datetime]): Filter by end time
1170
1177
  top_k (Optional[int]): Maximum number of results to return (default: 30)
1178
+ source (Optional[str]): Filter by profile source.
1179
+ profile_time_to_live (Optional[str]): Filter by profile TTL value.
1171
1180
  status_filter (Optional[list[Optional[Union[Status, str]]]]): Filter by profile status. Accepts Status enum or string values (e.g., "archived", "pending").
1172
1181
  tags (Optional[list[str]]): Match profiles having any of these tags.
1173
1182
 
@@ -1190,9 +1199,13 @@ class ReflexioClient:
1190
1199
  request,
1191
1200
  GetUserProfilesRequest,
1192
1201
  user_id=user_id,
1202
+ profile_id=profile_id,
1203
+ query=query,
1193
1204
  start_time=start_time,
1194
1205
  end_time=end_time,
1195
1206
  top_k=top_k,
1207
+ source=source,
1208
+ profile_time_to_live=profile_time_to_live,
1196
1209
  status_filter=converted_status_filter,
1197
1210
  tags=tags,
1198
1211
  )
@@ -1202,9 +1215,13 @@ class ReflexioClient:
1202
1215
  cached_result = self._cache.get(
1203
1216
  "get_profiles",
1204
1217
  user_id=req.user_id,
1218
+ profile_id=req.profile_id,
1219
+ query=req.query,
1205
1220
  start_time=req.start_time,
1206
1221
  end_time=req.end_time,
1207
1222
  top_k=req.top_k,
1223
+ source=req.source,
1224
+ profile_time_to_live=req.profile_time_to_live,
1208
1225
  status_filter=req.status_filter,
1209
1226
  tags=req.tags,
1210
1227
  )
@@ -1224,9 +1241,13 @@ class ReflexioClient:
1224
1241
  "get_profiles",
1225
1242
  result,
1226
1243
  user_id=req.user_id,
1244
+ profile_id=req.profile_id,
1245
+ query=req.query,
1227
1246
  start_time=req.start_time,
1228
1247
  end_time=req.end_time,
1229
1248
  top_k=req.top_k,
1249
+ source=req.source,
1250
+ profile_time_to_live=req.profile_time_to_live,
1230
1251
  status_filter=req.status_filter,
1231
1252
  tags=req.tags,
1232
1253
  )
@@ -1255,6 +1276,13 @@ class ReflexioClient:
1255
1276
  self,
1256
1277
  limit: int = 100,
1257
1278
  status_filter: str | None = None,
1279
+ user_id: str | None = None,
1280
+ profile_id: str | None = None,
1281
+ query: str | None = None,
1282
+ source: str | None = None,
1283
+ profile_time_to_live: str | None = None,
1284
+ start_time: int | None = None,
1285
+ end_time: int | None = None,
1258
1286
  ) -> GetProfilesViewResponse:
1259
1287
  """Get all user profiles across all users.
1260
1288
 
@@ -1263,6 +1291,13 @@ class ReflexioClient:
1263
1291
  status_filter (str, optional): Filter by profile status. Accepts
1264
1292
  ``"current"``, ``"pending"``, or ``"archived"``. If ``None``
1265
1293
  (the default), profiles with any status are returned.
1294
+ user_id (str, optional): Filter by exact user ID.
1295
+ profile_id (str, optional): Filter by exact profile ID.
1296
+ query (str, optional): Case-insensitive text filter across visible fields.
1297
+ source (str, optional): Filter by exact profile source.
1298
+ profile_time_to_live (str, optional): Filter by profile TTL value.
1299
+ start_time (int, optional): Minimum last-modified epoch seconds.
1300
+ end_time (int, optional): Maximum last-modified epoch seconds.
1266
1301
 
1267
1302
  Returns:
1268
1303
  GetProfilesViewResponse: Response containing all user profiles
@@ -1272,6 +1307,21 @@ class ReflexioClient:
1272
1307
  params: dict[str, str | int] = {"limit": limit}
1273
1308
  if status_filter:
1274
1309
  params["status_filter"] = status_filter
1310
+ params.update(
1311
+ {
1312
+ key: value
1313
+ for key, value in {
1314
+ "user_id": user_id,
1315
+ "profile_id": profile_id,
1316
+ "query": query,
1317
+ "source": source,
1318
+ "profile_time_to_live": profile_time_to_live,
1319
+ "start_time": start_time,
1320
+ "end_time": end_time,
1321
+ }.items()
1322
+ if value is not None
1323
+ }
1324
+ )
1275
1325
  response = self._make_request(
1276
1326
  "GET",
1277
1327
  f"/api/get_all_profiles?{urlencode(params)}",
@@ -1374,9 +1424,14 @@ class ReflexioClient:
1374
1424
  request: GetUserPlaybooksRequest | dict | None = None,
1375
1425
  *,
1376
1426
  limit: int | None = None,
1427
+ user_playbook_id: int | None = None,
1377
1428
  user_id: str | None = None,
1429
+ request_id: str | None = None,
1430
+ query: str | None = None,
1378
1431
  playbook_name: str | None = None,
1379
1432
  agent_version: str | None = None,
1433
+ start_time: datetime | None = None,
1434
+ end_time: datetime | None = None,
1380
1435
  status_filter: list[Status | None] | None = None,
1381
1436
  tags: list[str] | None = None,
1382
1437
  ) -> GetUserPlaybooksViewResponse:
@@ -1385,9 +1440,14 @@ class ReflexioClient:
1385
1440
  Args:
1386
1441
  request (Optional[GetUserPlaybooksRequest]): The get request object (alternative to kwargs)
1387
1442
  limit (Optional[int]): Maximum number of results to return (default: 100)
1443
+ user_playbook_id (Optional[int]): Exact user playbook ID filter.
1388
1444
  user_id (Optional[str]): Filter by user ID
1445
+ request_id (Optional[str]): Filter by generating request ID.
1446
+ query (Optional[str]): Case-insensitive text filter across visible fields.
1389
1447
  playbook_name (Optional[str]): Filter by playbook name
1390
1448
  agent_version (Optional[str]): Filter by agent version
1449
+ start_time (Optional[datetime]): Filter by start time.
1450
+ end_time (Optional[datetime]): Filter by end time.
1391
1451
  status_filter (Optional[list[Optional[Status]]]): Filter by status
1392
1452
  tags (Optional[list[str]]): Match playbooks having any of these tags.
1393
1453
 
@@ -1398,9 +1458,14 @@ class ReflexioClient:
1398
1458
  request,
1399
1459
  GetUserPlaybooksRequest,
1400
1460
  limit=limit,
1461
+ user_playbook_id=user_playbook_id,
1401
1462
  user_id=user_id,
1463
+ request_id=request_id,
1464
+ query=query,
1402
1465
  playbook_name=playbook_name,
1403
1466
  agent_version=agent_version,
1467
+ start_time=start_time,
1468
+ end_time=end_time,
1404
1469
  status_filter=status_filter,
1405
1470
  tags=tags,
1406
1471
  )
@@ -1652,8 +1717,12 @@ class ReflexioClient:
1652
1717
  force_refresh: bool = False,
1653
1718
  *,
1654
1719
  limit: int | None = None,
1720
+ agent_playbook_id: int | None = None,
1721
+ query: str | None = None,
1655
1722
  playbook_name: str | None = None,
1656
1723
  agent_version: str | None = None,
1724
+ start_time: datetime | None = None,
1725
+ end_time: datetime | None = None,
1657
1726
  status_filter: list[Status | None] | None = None,
1658
1727
  playbook_status_filter: PlaybookStatus | None = None,
1659
1728
  tags: list[str] | None = None,
@@ -1664,8 +1733,12 @@ class ReflexioClient:
1664
1733
  request (Optional[GetAgentPlaybooksRequest]): The get request object (alternative to kwargs)
1665
1734
  force_refresh (bool, optional): If True, bypass cache and fetch fresh data. Defaults to False.
1666
1735
  limit (Optional[int]): Maximum number of results to return (default: 100)
1736
+ agent_playbook_id (Optional[int]): Exact agent playbook ID filter.
1737
+ query (Optional[str]): Case-insensitive text filter across visible fields.
1667
1738
  playbook_name (Optional[str]): Filter by playbook name
1668
1739
  agent_version (Optional[str]): Filter by agent version
1740
+ start_time (Optional[datetime]): Filter by start time.
1741
+ end_time (Optional[datetime]): Filter by end time.
1669
1742
  status_filter (Optional[list[Optional[Status]]]): Filter by status
1670
1743
  playbook_status_filter (Optional[PlaybookStatus]): Filter by playbook status (default: APPROVED)
1671
1744
  tags (Optional[list[str]]): Match playbooks having any of these tags.
@@ -1677,8 +1750,12 @@ class ReflexioClient:
1677
1750
  request,
1678
1751
  GetAgentPlaybooksRequest,
1679
1752
  limit=limit,
1753
+ agent_playbook_id=agent_playbook_id,
1754
+ query=query,
1680
1755
  playbook_name=playbook_name,
1681
1756
  agent_version=agent_version,
1757
+ start_time=start_time,
1758
+ end_time=end_time,
1682
1759
  status_filter=status_filter,
1683
1760
  playbook_status_filter=playbook_status_filter,
1684
1761
  tags=tags,
@@ -1689,8 +1766,12 @@ class ReflexioClient:
1689
1766
  cached_result = self._cache.get(
1690
1767
  "get_agent_playbooks",
1691
1768
  limit=req.limit,
1769
+ agent_playbook_id=req.agent_playbook_id,
1770
+ query=req.query,
1692
1771
  playbook_name=req.playbook_name,
1693
1772
  agent_version=req.agent_version,
1773
+ start_time=req.start_time,
1774
+ end_time=req.end_time,
1694
1775
  status_filter=req.status_filter,
1695
1776
  playbook_status_filter=req.playbook_status_filter,
1696
1777
  tags=req.tags,
@@ -1711,8 +1792,12 @@ class ReflexioClient:
1711
1792
  "get_agent_playbooks",
1712
1793
  result,
1713
1794
  limit=req.limit,
1795
+ agent_playbook_id=req.agent_playbook_id,
1796
+ query=req.query,
1714
1797
  playbook_name=req.playbook_name,
1715
1798
  agent_version=req.agent_version,
1799
+ start_time=req.start_time,
1800
+ end_time=req.end_time,
1716
1801
  status_filter=req.status_filter,
1717
1802
  playbook_status_filter=req.playbook_status_filter,
1718
1803
  tags=req.tags,
@@ -1726,6 +1811,8 @@ class ReflexioClient:
1726
1811
  *,
1727
1812
  user_id: str | None = None,
1728
1813
  request_id: str | None = None,
1814
+ session_id: str | None = None,
1815
+ source: str | None = None,
1729
1816
  start_time: datetime | None = None,
1730
1817
  end_time: datetime | None = None,
1731
1818
  top_k: int | None = None,
@@ -1736,6 +1823,8 @@ class ReflexioClient:
1736
1823
  request (Optional[GetRequestsRequest]): The get request object (alternative to kwargs)
1737
1824
  user_id (Optional[str]): Filter by user ID
1738
1825
  request_id (Optional[str]): Filter by request ID
1826
+ session_id (Optional[str]): Filter by session ID
1827
+ source (Optional[str]): Filter by request source
1739
1828
  start_time (Optional[datetime]): Filter by start time
1740
1829
  end_time (Optional[datetime]): Filter by end time
1741
1830
  top_k (Optional[int]): Maximum number of results to return (default: 30)
@@ -1748,6 +1837,8 @@ class ReflexioClient:
1748
1837
  GetRequestsRequest,
1749
1838
  user_id=user_id,
1750
1839
  request_id=request_id,
1840
+ session_id=session_id,
1841
+ source=source,
1751
1842
  start_time=start_time,
1752
1843
  end_time=end_time,
1753
1844
  top_k=top_k,
@@ -1765,6 +1856,8 @@ class ReflexioClient:
1765
1856
  *,
1766
1857
  limit: int | None = None,
1767
1858
  agent_version: str | None = None,
1859
+ start_time: datetime | None = None,
1860
+ end_time: datetime | None = None,
1768
1861
  ) -> GetEvaluationResultsViewResponse:
1769
1862
  """Get agent success evaluation results.
1770
1863
 
@@ -1772,6 +1865,8 @@ class ReflexioClient:
1772
1865
  request (Optional[GetAgentSuccessEvaluationResultsRequest]): The get request object (alternative to kwargs)
1773
1866
  limit (Optional[int]): Maximum number of results to return (default: 100)
1774
1867
  agent_version (Optional[str]): Filter by agent version
1868
+ start_time (Optional[datetime]): Filter by start time
1869
+ end_time (Optional[datetime]): Filter by end time
1775
1870
 
1776
1871
  Returns:
1777
1872
  GetEvaluationResultsViewResponse: Response containing agent success evaluation results
@@ -1781,6 +1876,8 @@ class ReflexioClient:
1781
1876
  GetAgentSuccessEvaluationResultsRequest,
1782
1877
  limit=limit,
1783
1878
  agent_version=agent_version,
1879
+ start_time=start_time,
1880
+ end_time=end_time,
1784
1881
  )
1785
1882
  response = self._make_request(
1786
1883
  "POST",
@@ -213,8 +213,16 @@ class AgentPlaybookMixin(ReflexioBase):
213
213
  try:
214
214
  agent_playbooks = self._get_storage().get_agent_playbooks(
215
215
  limit=request.limit or 100,
216
+ agent_playbook_id=request.agent_playbook_id,
217
+ query=request.query,
216
218
  playbook_name=request.playbook_name,
217
219
  agent_version=request.agent_version,
220
+ start_time=(
221
+ int(request.start_time.timestamp()) if request.start_time else None
222
+ ),
223
+ end_time=(
224
+ int(request.end_time.timestamp()) if request.end_time else None
225
+ ),
218
226
  status_filter=request.status_filter,
219
227
  playbook_status_filter=[request.playbook_status_filter]
220
228
  if request.playbook_status_filter
@@ -127,6 +127,21 @@ class ReflexioBase:
127
127
  raise RuntimeError(STORAGE_NOT_CONFIGURED_MSG)
128
128
  return storage
129
129
 
130
+ def get_storage(self) -> BaseStorage:
131
+ """Return the configured storage backend.
132
+
133
+ Public accessor for consumers (including enterprise) that need the
134
+ underlying ``BaseStorage``. Prefer ``request_context.storage`` inside
135
+ OSS pipeline code; this exists for callers that hold a ``Reflexio``.
136
+
137
+ Returns:
138
+ BaseStorage: The configured storage backend.
139
+
140
+ Raises:
141
+ RuntimeError: If storage is not configured.
142
+ """
143
+ return self._get_storage()
144
+
130
145
  def _get_query_reformulator(self) -> QueryReformulator:
131
146
  """Lazily create and cache a QueryReformulator instance.
132
147