clsplusplus 7.3.0__tar.gz → 7.3.1__tar.gz

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 (179) hide show
  1. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/PKG-INFO +1 -1
  2. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/pyproject.toml +1 -1
  3. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/api.py +14 -0
  4. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/PKG-INFO +1 -1
  5. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/SOURCES.txt +1 -0
  6. clsplusplus-7.3.1/tests/test_extension_cross_model_runtime.py +280 -0
  7. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_extension_integration.py +26 -2
  8. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_extension_ui.py +1 -1
  9. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/LICENSE +0 -0
  10. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/README.md +0 -0
  11. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/setup.cfg +0 -0
  12. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/__init__.py +0 -0
  13. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/abuse_guard.py +0 -0
  14. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/api_usage_analytics.py +0 -0
  15. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/auth.py +0 -0
  16. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/cli.py +0 -0
  17. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/client.py +0 -0
  18. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/config.py +0 -0
  19. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/cost_forecast.py +0 -0
  20. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/cost_model.py +0 -0
  21. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/debug_console.py +0 -0
  22. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/demo_llm.py +0 -0
  23. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/demo_llm_calls.py +0 -0
  24. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/demo_local.py +0 -0
  25. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/email_service.py +0 -0
  26. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/embeddings.py +0 -0
  27. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/funnel_routes.py +0 -0
  28. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/geo.py +0 -0
  29. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/health_metrics.py +0 -0
  30. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/homepage_autopromote.py +0 -0
  31. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/idempotency.py +0 -0
  32. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integration_service.py +0 -0
  33. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/__init__.py +0 -0
  34. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/autogen.py +0 -0
  35. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/crewai.py +0 -0
  36. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/langgraph.py +0 -0
  37. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/llamaindex.py +0 -0
  38. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/jwt_utils.py +0 -0
  39. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/local_routes.py +0 -0
  40. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/main.py +0 -0
  41. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/mcp_http.py +0 -0
  42. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/mcp_server.py +0 -0
  43. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/memory_cycle.py +0 -0
  44. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/memory_phase.py +0 -0
  45. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/memory_service.py +0 -0
  46. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/__init__.py +0 -0
  47. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/__main__.py +0 -0
  48. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/billing.py +0 -0
  49. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/healthcheck.py +0 -0
  50. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/notifier.py +0 -0
  51. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/pricing.py +0 -0
  52. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/reconciler.py +0 -0
  53. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/schema.py +0 -0
  54. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/writer.py +0 -0
  55. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metrics.py +0 -0
  56. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/middleware.py +0 -0
  57. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/models.py +0 -0
  58. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/namespace_resolver.py +0 -0
  59. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/oauth_server.py +0 -0
  60. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/permissions.py +0 -0
  61. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/plasticity.py +0 -0
  62. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/pricing.py +0 -0
  63. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/pricing_models.py +0 -0
  64. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/pricing_store.py +0 -0
  65. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/prompt_log.py +0 -0
  66. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/rate_limit.py +0 -0
  67. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/razorpay_service.py +0 -0
  68. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/rbac_service.py +0 -0
  69. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/reconsolidation.py +0 -0
  70. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/resilience.py +0 -0
  71. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/sleep_cycle.py +0 -0
  72. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/__init__.py +0 -0
  73. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/base.py +0 -0
  74. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/chat_session_store.py +0 -0
  75. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/integration_store.py +0 -0
  76. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l0_working_buffer.py +0 -0
  77. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l1_indexing_store.py +0 -0
  78. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l2_schema_graph.py +0 -0
  79. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l3_deep_recess.py +0 -0
  80. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l3_postgres.py +0 -0
  81. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/oauth_store.py +0 -0
  82. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/rbac_store.py +0 -0
  83. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/user_store.py +0 -0
  84. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/waitlist_store.py +0 -0
  85. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/web_events_store.py +0 -0
  86. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stripe_service.py +0 -0
  87. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/subscription_watchdog.py +0 -0
  88. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/temporal.py +0 -0
  89. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/test_suite.py +0 -0
  90. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/tier_resolver.py +0 -0
  91. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/tiers.py +0 -0
  92. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/topical_resonance.py +0 -0
  93. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/tracer.py +0 -0
  94. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/usage.py +0 -0
  95. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/user_embeddings.py +0 -0
  96. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/user_pulse.py +0 -0
  97. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/user_service.py +0 -0
  98. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/waitlist_service.py +0 -0
  99. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/webhook_dispatcher.py +0 -0
  100. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/weblab.py +0 -0
  101. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/weblab_watcher.py +0 -0
  102. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/window_limits.py +0 -0
  103. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/write_metrics.py +0 -0
  104. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/dependency_links.txt +0 -0
  105. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/entry_points.txt +0 -0
  106. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/requires.txt +0 -0
  107. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/top_level.txt +0 -0
  108. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_abuse_guard.py +0 -0
  109. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_admin.py +0 -0
  110. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_admin_seed.py +0 -0
  111. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_api.py +0 -0
  112. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_api_comprehensive.py +0 -0
  113. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_api_endpoints.py +0 -0
  114. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_api_usage_analytics.py +0 -0
  115. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_auth.py +0 -0
  116. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_auth_me_api_key.py +0 -0
  117. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_autogen_memory.py +0 -0
  118. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_billing_e2e.py +0 -0
  119. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_burst_hardening.py +0 -0
  120. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_client_sdk.py +0 -0
  121. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_config.py +0 -0
  122. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_cost_forecast.py +0 -0
  123. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_crewai_provider.py +0 -0
  124. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_cross_llm_memory.py +0 -0
  125. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_debug_console.py +0 -0
  126. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_demo_llm.py +0 -0
  127. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_embeddings.py +0 -0
  128. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_feedback_github.py +0 -0
  129. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_free_tier_lifecycle.py +0 -0
  130. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_full_api_coverage.py +0 -0
  131. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_funnel_metrics.py +0 -0
  132. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_geo_gating.py +0 -0
  133. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_health_metrics.py +0 -0
  134. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_idempotency.py +0 -0
  135. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_integration_service.py +0 -0
  136. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_integration_sync_cli_contract.py +0 -0
  137. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_langgraph_store.py +0 -0
  138. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_llamaindex_block.py +0 -0
  139. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_mcp_connect.py +0 -0
  140. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_mcp_oauth.py +0 -0
  141. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_memory_cycle.py +0 -0
  142. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_memory_phase.py +0 -0
  143. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_memory_service.py +0 -0
  144. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_memory_write_no_schema_garbage.py +0 -0
  145. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_healthcheck.py +0 -0
  146. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_notifier.py +0 -0
  147. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_pricing.py +0 -0
  148. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_reconciler.py +0 -0
  149. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_schema.py +0 -0
  150. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_writer.py +0 -0
  151. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_middleware.py +0 -0
  152. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_models.py +0 -0
  153. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_overage_billing.py +0 -0
  154. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_performance.py +0 -0
  155. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_plasticity.py +0 -0
  156. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_pricing.py +0 -0
  157. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_prototype_e2e.py +0 -0
  158. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_rate_limit.py +0 -0
  159. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_razorpay_billing.py +0 -0
  160. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_razorpay_subscription_webhooks.py +0 -0
  161. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_reconsolidation.py +0 -0
  162. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_regression.py +0 -0
  163. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_resilience.py +0 -0
  164. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_security.py +0 -0
  165. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_sleep_cycle.py +0 -0
  166. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_stores.py +0 -0
  167. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_subscription_watchdog.py +0 -0
  168. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_tier_resolver.py +0 -0
  169. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_tiers.py +0 -0
  170. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_usage.py +0 -0
  171. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_user_auth.py +0 -0
  172. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_user_embeddings.py +0 -0
  173. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_user_pulse.py +0 -0
  174. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_user_stories.py +0 -0
  175. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_waitlist.py +0 -0
  176. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_webhook_dispatcher.py +0 -0
  177. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_weblab.py +0 -0
  178. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_window_limits.py +0 -0
  179. {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_write_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clsplusplus
3
- Version: 7.3.0
3
+ Version: 7.3.1
4
4
  Summary: Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama.
5
5
  Author-email: AlphaForge AI Labs <contact@alphaforge.ai>
6
6
  Maintainer-email: Rajamohan Jabbala <contact@alphaforge.ai>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clsplusplus"
7
- version = "7.3.0"
7
+ version = "7.3.1"
8
8
  description = "Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -481,6 +481,20 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
481
481
  "distinct_id": distinct_id,
482
482
  }
483
483
 
484
+ @app.get("/v1/config/extension")
485
+ async def extension_provider_config():
486
+ """Hot-updatable provider URL rules for the Chrome extension.
487
+
488
+ When ChatGPT/Claude/Gemini change API paths, update extension/provider_rules.json
489
+ on the server — clients pick it up without a Chrome Web Store redeploy.
490
+ """
491
+ rules_path = FilePath(__file__).resolve().parents[2] / "extension" / "provider_rules.json"
492
+ try:
493
+ import json as _json
494
+ return _json.loads(rules_path.read_text(encoding="utf-8"))
495
+ except Exception:
496
+ return {"version": 0, "providers": {}}
497
+
484
498
  @app.get("/admin/weblab")
485
499
  async def admin_weblab_status(request: Request):
486
500
  """Weblab control status — watched flags + latest green/red verdict."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clsplusplus
3
- Version: 7.3.0
3
+ Version: 7.3.1
4
4
  Summary: Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama.
5
5
  Author-email: AlphaForge AI Labs <contact@alphaforge.ai>
6
6
  Maintainer-email: Rajamohan Jabbala <contact@alphaforge.ai>
@@ -120,6 +120,7 @@ tests/test_cross_llm_memory.py
120
120
  tests/test_debug_console.py
121
121
  tests/test_demo_llm.py
122
122
  tests/test_embeddings.py
123
+ tests/test_extension_cross_model_runtime.py
123
124
  tests/test_extension_integration.py
124
125
  tests/test_extension_ui.py
125
126
  tests/test_feedback_github.py
@@ -0,0 +1,280 @@
1
+ """
2
+ Executable cross-model extension runtime tests.
3
+
4
+ Simulates the ChatGPT → Claude handoff without a browser:
5
+ store on GPT-shaped body, prefetch mailbox, inject on Claude-shaped bodies.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import time
14
+ from copy import deepcopy
15
+
16
+ import pytest
17
+
18
+ EXT_DIR = os.path.join(os.path.dirname(__file__), "..", "extension")
19
+ CTX_MARKER = "For context, here are some things I have mentioned before"
20
+
21
+
22
+ def read_ext(name: str) -> str:
23
+ with open(os.path.join(EXT_DIR, name), "r") as f:
24
+ return f.read()
25
+
26
+
27
+ def build_ctx(facts: list[str]) -> str:
28
+ return (
29
+ "For context, here are some things I have mentioned before in other conversations:\n"
30
+ + "\n".join(f"- {f[:200]}" for f in facts)
31
+ + "\n\nNow, my actual question:\n"
32
+ )
33
+
34
+
35
+ def is_llm(url: str, host: str) -> bool:
36
+ u = url.lower()
37
+ h = host.lower()
38
+ if "chatgpt" in h or "openai" in h:
39
+ if "/backend-api/" not in u or "conversation" not in u:
40
+ return False
41
+ if "/init" in u or "/prepare" in u:
42
+ return False
43
+ return True
44
+ if "claude" in h:
45
+ if "/v1/sessions/" in u and "/events" in u:
46
+ return True
47
+ if "/api/organizations/" in u and "chat_conversations" in u and "completion" in u:
48
+ return True
49
+ return "/api/" in u and any(k in u for k in ("chat", "completion", "message"))
50
+ if "gemini" in h:
51
+ return "batchexecute" in u
52
+ return False
53
+
54
+
55
+ def extract(body: str, host: str) -> tuple[str, callable] | None:
56
+ try:
57
+ b = json.loads(body)
58
+ except json.JSONDecodeError:
59
+ return None
60
+
61
+ if "chatgpt" in host or "openai" in host:
62
+ messages = b.get("messages") or []
63
+ if messages:
64
+ m = messages[-1]
65
+ content = m.get("content")
66
+ if isinstance(content, dict) and content.get("parts"):
67
+ q = content["parts"][0]
68
+ def setter(ctx, _b=b, _m=m):
69
+ _m["content"]["parts"][0] = ctx + q
70
+ return json.dumps(_b)
71
+ return q, setter
72
+ if isinstance(content, str):
73
+ def setter(ctx, _b=b, _m=m):
74
+ _m["content"] = ctx + content
75
+ return json.dumps(_b)
76
+ return content, setter
77
+
78
+ if isinstance(b.get("prompt"), str) and len(b["prompt"]) > 3:
79
+ q = b["prompt"]
80
+ def setter(ctx, _b=b):
81
+ _b["prompt"] = ctx + q
82
+ return json.dumps(_b)
83
+ return q, setter
84
+
85
+ events = b.get("events")
86
+ if isinstance(events, list):
87
+ for ev in reversed(events):
88
+ if not ev or ev.get("type") != "user":
89
+ continue
90
+ msg = ev.get("message") or {}
91
+ content = msg.get("content")
92
+ if isinstance(content, str) and content:
93
+ def setter(ctx, _msg=msg, _q=content, _b=b):
94
+ _msg["content"] = ctx + _q
95
+ return json.dumps(_b)
96
+ return content, setter
97
+ if isinstance(content, list):
98
+ for block in content:
99
+ if block.get("type") == "text" and block.get("text"):
100
+ q = block["text"]
101
+ def setter(ctx, _block=block, _q=q, _b=b):
102
+ _block["text"] = ctx + _q
103
+ return json.dumps(_b)
104
+ return q, setter
105
+ return None
106
+
107
+
108
+ def inject(body: str, host: str, facts: list[str]) -> str | None:
109
+ parsed = extract(body, host)
110
+ if not parsed:
111
+ return None
112
+ _q, setter = parsed
113
+ return setter(build_ctx(facts))
114
+
115
+
116
+ class TestURLDetection:
117
+ def test_chatgpt_conversation(self):
118
+ assert is_llm("https://chatgpt.com/backend-api/conversation", "chatgpt.com")
119
+
120
+ def test_claude_completion_legacy(self):
121
+ url = "https://claude.ai/api/organizations/org/chat_conversations/conv/completion"
122
+ assert is_llm(url, "claude.ai")
123
+
124
+ def test_claude_sessions_events_2026(self):
125
+ url = "https://claude.ai/v1/sessions/session_01ABC/events"
126
+ assert is_llm(url, "claude.ai")
127
+
128
+ def test_claude_org_list_not_llm(self):
129
+ assert not is_llm("https://claude.ai/api/organizations", "claude.ai")
130
+
131
+
132
+ class TestCrossModelInjection:
133
+ def test_chatgpt_store_shape_injects(self):
134
+ body = json.dumps({
135
+ "messages": [{
136
+ "role": "user",
137
+ "content": {"parts": ["My favorite color is teal"]},
138
+ }]
139
+ })
140
+ out = inject(body, "chatgpt.com", ["My favorite color is teal", "I live in Austin"])
141
+ assert out
142
+ assert CTX_MARKER in out
143
+ assert "teal" in out
144
+
145
+ def test_claude_completion_injects(self):
146
+ body = json.dumps({"prompt": "What is my favorite color?"})
147
+ out = inject(body, "claude.ai", ["My favorite color is teal"])
148
+ assert out
149
+ assert CTX_MARKER in out
150
+ assert "teal" in out
151
+
152
+ def test_claude_sessions_events_injects(self):
153
+ body = json.dumps({
154
+ "events": [{
155
+ "type": "user",
156
+ "message": {"role": "user", "content": "What is my favorite color?"},
157
+ }]
158
+ })
159
+ out = inject(body, "claude.ai", ["My favorite color is teal"])
160
+ assert out
161
+ assert CTX_MARKER in out
162
+ assert "teal" in out
163
+
164
+ def test_claude_sessions_blocks_injects(self):
165
+ body = json.dumps({
166
+ "events": [{
167
+ "type": "user",
168
+ "message": {
169
+ "role": "user",
170
+ "content": [{"type": "text", "text": "Remind me where I live"}],
171
+ },
172
+ }]
173
+ })
174
+ out = inject(body, "claude.ai", ["I live in Springfield"])
175
+ assert out
176
+ assert "Springfield" in out
177
+
178
+
179
+ class TestMailboxRace:
180
+ def test_delayed_mailbox_still_injects_within_wait_window(self):
181
+ """Simulates capture.js prefetch arriving after GPT store but before Claude send."""
182
+ facts: list[str] = []
183
+ mailbox_ready_at = time.time() + 0.15
184
+
185
+ def get_ctx_now() -> str:
186
+ if time.time() < mailbox_ready_at or not facts:
187
+ return ""
188
+ return build_ctx(facts)
189
+
190
+ def wait_for_ctx(timeout_ms: int = 1200) -> str:
191
+ deadline = time.time() + timeout_ms / 1000
192
+ while time.time() < deadline:
193
+ ctx = get_ctx_now()
194
+ if ctx:
195
+ return ctx
196
+ time.sleep(0.04)
197
+ return ""
198
+
199
+ # GPT turn stores fact (async)
200
+ facts.append("My dog is named Max")
201
+
202
+ claude_body = json.dumps({"prompt": "What is my dog's name?"})
203
+ parsed = extract(claude_body, "claude.ai")
204
+ assert parsed
205
+ q, setter = parsed
206
+
207
+ ctx = get_ctx_now()
208
+ if not ctx:
209
+ ctx = wait_for_ctx()
210
+ assert ctx
211
+ out = setter(ctx)
212
+ assert "Max" in out
213
+
214
+
215
+ class TestExtensionSourceGuards:
216
+ def test_intercept_has_sessions_extractor(self):
217
+ js = read_ext("intercept.js")
218
+ assert "b.events" in js
219
+ assert "/v1/sessions/" in js
220
+
221
+ def test_intercept_async_uses_wait_path(self):
222
+ js = read_ext("intercept.js")
223
+ assert "async function injectBody" in js
224
+ assert "await injectBody(body, 'async')" in js
225
+
226
+ def test_background_bumps_mailbox_after_store(self):
227
+ js = read_ext("background.js")
228
+ assert "cls_mem_bump" in js
229
+
230
+ def test_capture_listens_for_mem_bump(self):
231
+ js = read_ext("capture.js")
232
+ assert "cls_mem_bump" in js
233
+
234
+ def test_query_aware_recall_on_send(self):
235
+ js = read_ext("capture.js")
236
+ assert "data-cls-query" in js
237
+ assert "FETCH_RELEVANT" in js
238
+ assert "refreshMemoriesForQuery" in js
239
+
240
+ def test_remote_provider_rules_sync(self):
241
+ js = read_ext("capture.js")
242
+ assert "data-cls-provider-rules" in js
243
+ assert "GET_PROVIDER_RULES" in js
244
+ js2 = read_ext("intercept.js")
245
+ assert "getProviderRules" in js2
246
+ js3 = read_ext("background.js")
247
+ assert "/v1/config/extension" in js3
248
+
249
+
250
+ @pytest.mark.asyncio
251
+ async def test_prototype_mock_claude_sessions_endpoint():
252
+ """Server mock validates injected sessions API body."""
253
+ import sys
254
+ from pathlib import Path
255
+ from httpx import ASGITransport, AsyncClient
256
+
257
+ root = Path(__file__).resolve().parents[1]
258
+ for p in (str(root / "prototype"), str(root / "src")):
259
+ if p not in sys.path:
260
+ sys.path.insert(0, p)
261
+ import server as proto_server # noqa: PLC0415
262
+
263
+ transport = ASGITransport(app=proto_server.app)
264
+ async with AsyncClient(transport=transport, base_url="http://test") as client:
265
+ body = {
266
+ "events": [{
267
+ "type": "user",
268
+ "message": {
269
+ "role": "user",
270
+ "content": (
271
+ "For context, here are some things I have mentioned before in other conversations:\n"
272
+ "- My favorite color is teal\n\n"
273
+ "Now, my actual question:\nWhat is my favorite color?"
274
+ ),
275
+ },
276
+ }]
277
+ }
278
+ r = await client.post("/v1/sessions/session_test/events", json=body)
279
+ assert r.status_code == 200
280
+ assert r.json().get("injection_ok") is True
@@ -90,7 +90,8 @@ class TestInterceptClaudeExtraction:
90
90
 
91
91
  def test_detects_claude_url(self):
92
92
  assert "claude" in self.js
93
- assert "/api/" in self.js
93
+ assert "/v1/sessions/" in self.js
94
+ assert "chat_conversations" in self.js
94
95
 
95
96
  def test_extracts_prompt_field(self):
96
97
  assert "b.prompt" in self.js
@@ -106,6 +107,12 @@ class TestInterceptClaudeExtraction:
106
107
  def test_extracts_text_field(self):
107
108
  assert "b.text" in self.js
108
109
 
110
+ def test_has_short_wait_fallback_for_first_claude_send(self):
111
+ """Injection waits for mailbox when facts are not ready yet."""
112
+ assert "waitForCtx" in self.js
113
+ assert "async function injectBody" in self.js
114
+ assert "await injectBody(body, 'async')" in self.js
115
+
109
116
 
110
117
  class TestInterceptGeminiExtraction:
111
118
  """Verify Gemini batchexecute URL-encoded form data extraction."""
@@ -212,6 +219,10 @@ class TestCaptureDOMMailbox:
212
219
  def test_writes_timestamp(self):
213
220
  assert "data-ts" in self.js
214
221
 
222
+ def test_uses_bridge_node_fallback_before_body_exists(self):
223
+ """Early document_start execution may run before body is available."""
224
+ assert "document.body || document.documentElement" in self.js
225
+
215
226
  def test_reads_outbox(self):
216
227
  assert "data-cls-outbox" in self.js
217
228
  assert "JSON.parse" in self.js
@@ -365,7 +376,8 @@ class TestBackgroundMessageRouting:
365
376
 
366
377
  def test_handles_all_message_types(self):
367
378
  required = [
368
- "STORE", "FETCH", "ACTIVITY", "USAGE", "SEARCH",
379
+ "STORE", "FETCH", "FETCH_RELEVANT", "GET_PROVIDER_RULES",
380
+ "ACTIVITY", "USAGE", "SEARCH",
369
381
  "DAILY_COUNTERS", "INCREMENT_INJECTED", "VERIFY_KEY", "OPEN_PANEL",
370
382
  ]
371
383
  for msg_type in required:
@@ -431,6 +443,12 @@ class TestCrossWorldArchitecture:
431
443
  assert main[0]["js"] == ["intercept.js"]
432
444
  assert isolated[0]["js"] == ["capture.js"]
433
445
 
446
+ def test_capture_runs_at_document_start(self):
447
+ """Capture must initialize mailbox before first provider send."""
448
+ manifest = json.loads(read_ext("manifest.json"))
449
+ capture = [s for s in manifest["content_scripts"] if s.get("js") == ["capture.js"]][0]
450
+ assert capture["run_at"] == "document_start"
451
+
434
452
 
435
453
  # ═══════════════════════════════════════════════════════════════════════════════
436
454
  # Memory Filter Logic (capture.js)
@@ -457,3 +475,9 @@ class TestMemoryFiltering:
457
475
 
458
476
  def test_filters_questions(self):
459
477
  assert "?" in self.js # endsWith('?') filter
478
+
479
+ def test_refreshes_on_tab_reentry(self):
480
+ """Switching models/tabs should trigger immediate prefetch refresh."""
481
+ assert "visibilitychange" in self.js
482
+ assert "pageshow" in self.js
483
+ assert "focus" in self.js
@@ -648,7 +648,7 @@ class TestCaptureJS:
648
648
  assert "data-facts" in self.js
649
649
 
650
650
  def test_refresh_interval(self):
651
- assert "setInterval(refreshMemories, 30000)" in self.js
651
+ assert "setInterval(refreshMemories, 10000)" in self.js
652
652
 
653
653
  def test_outbox_poll_interval(self):
654
654
  assert "setInterval(checkOutbox, 500)" in self.js
File without changes
File without changes
File without changes