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.
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/PKG-INFO +1 -1
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/pyproject.toml +1 -1
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/api.py +14 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/PKG-INFO +1 -1
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/SOURCES.txt +1 -0
- clsplusplus-7.3.1/tests/test_extension_cross_model_runtime.py +280 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_extension_integration.py +26 -2
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_extension_ui.py +1 -1
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/LICENSE +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/README.md +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/setup.cfg +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/__init__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/abuse_guard.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/api_usage_analytics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/auth.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/cli.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/client.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/config.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/cost_forecast.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/cost_model.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/debug_console.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/demo_llm.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/demo_llm_calls.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/demo_local.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/email_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/embeddings.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/funnel_routes.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/geo.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/health_metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/homepage_autopromote.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/idempotency.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integration_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/__init__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/autogen.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/crewai.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/langgraph.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/integrations/llamaindex.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/jwt_utils.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/local_routes.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/main.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/mcp_http.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/mcp_server.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/memory_cycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/memory_phase.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/memory_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/__init__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/__main__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/billing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/healthcheck.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/notifier.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/pricing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/reconciler.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/schema.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metering_v2/writer.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/middleware.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/models.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/namespace_resolver.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/oauth_server.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/permissions.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/plasticity.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/pricing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/pricing_models.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/pricing_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/prompt_log.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/rate_limit.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/razorpay_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/rbac_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/reconsolidation.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/resilience.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/sleep_cycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/__init__.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/base.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/chat_session_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/integration_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l0_working_buffer.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l1_indexing_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l2_schema_graph.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l3_deep_recess.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/l3_postgres.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/oauth_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/rbac_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/user_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/waitlist_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stores/web_events_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/stripe_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/subscription_watchdog.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/temporal.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/test_suite.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/tier_resolver.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/tiers.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/topical_resonance.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/tracer.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/usage.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/user_embeddings.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/user_pulse.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/user_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/waitlist_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/webhook_dispatcher.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/weblab.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/weblab_watcher.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/window_limits.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus/write_metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/dependency_links.txt +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/entry_points.txt +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/requires.txt +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/src/clsplusplus.egg-info/top_level.txt +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_abuse_guard.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_admin.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_admin_seed.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_api.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_api_comprehensive.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_api_endpoints.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_api_usage_analytics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_auth.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_auth_me_api_key.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_autogen_memory.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_billing_e2e.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_burst_hardening.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_client_sdk.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_config.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_cost_forecast.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_crewai_provider.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_cross_llm_memory.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_debug_console.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_demo_llm.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_embeddings.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_feedback_github.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_free_tier_lifecycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_full_api_coverage.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_funnel_metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_geo_gating.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_health_metrics.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_idempotency.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_integration_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_integration_sync_cli_contract.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_langgraph_store.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_llamaindex_block.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_mcp_connect.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_mcp_oauth.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_memory_cycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_memory_phase.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_memory_service.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_memory_write_no_schema_garbage.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_healthcheck.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_notifier.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_pricing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_reconciler.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_schema.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_metering_v2_writer.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_middleware.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_models.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_overage_billing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_performance.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_plasticity.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_pricing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_prototype_e2e.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_rate_limit.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_razorpay_billing.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_razorpay_subscription_webhooks.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_reconsolidation.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_regression.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_resilience.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_security.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_sleep_cycle.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_stores.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_subscription_watchdog.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_tier_resolver.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_tiers.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_usage.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_user_auth.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_user_embeddings.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_user_pulse.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_user_stories.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_waitlist.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_webhook_dispatcher.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_weblab.py +0 -0
- {clsplusplus-7.3.0 → clsplusplus-7.3.1}/tests/test_window_limits.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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 "/
|
|
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", "
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|