diff --git a/plugin/core/client.py b/plugin/core/client.py index 7bade23..00bd8e8 100644 --- a/plugin/core/client.py +++ b/plugin/core/client.py @@ -8,6 +8,8 @@ from engram import EngramClient +from .client_origin import client_origin_header + DEFAULT_BASE = "https://api.engram.weaviate.io" _PROFILES = ( @@ -91,7 +93,9 @@ def get_client(): api_key = engram_api_key() if not api_key: return None - return EngramClient(api_key=api_key, base_url=engram_base_url()) + return EngramClient( + api_key=api_key, base_url=engram_base_url(), headers=client_origin_header() + ) def engram_warning(): @@ -110,7 +114,11 @@ def engram_get(path): whole instead of continuing with a half-resolved scope.""" base = engram_base_url().rstrip("/") req = urllib.request.Request( - base + path, headers={"Authorization": f"Bearer {engram_api_key()}"} + base + path, + headers={ + "Authorization": f"Bearer {engram_api_key()}", + **client_origin_header(), + }, ) with urllib.request.urlopen(req, timeout=5) as resp: return json.load(resp) diff --git a/plugin/core/client_origin.py b/plugin/core/client_origin.py new file mode 100644 index 0000000..cecad5f --- /dev/null +++ b/plugin/core/client_origin.py @@ -0,0 +1,22 @@ +import json +import os + +_MANIFEST = os.path.join( + os.path.dirname(__file__), "..", ".claude-plugin", "plugin.json" +) + + +def _platform(): + return "claude" + + +def _plugin_version(): + try: + with open(_MANIFEST) as f: + return json.load(f).get("version", "unknown") + except Exception: + return "unknown" + + +def client_origin_header(): + return {"X-Engram-Client": f"{_platform()}-plugin/{_plugin_version()}"} diff --git a/plugin/tests/test_client_origin.py b/plugin/tests/test_client_origin.py new file mode 100644 index 0000000..06664b5 --- /dev/null +++ b/plugin/tests/test_client_origin.py @@ -0,0 +1,44 @@ +"""Loads the module directly by path — importing the `core` package would pull in the +Engram SDK, which only exists in the plugin venv.""" + +import importlib.util +import json +import os +import unittest +from unittest import mock + +_HERE = os.path.dirname(__file__) +_MODULE = os.path.join(_HERE, "..", "core", "client_origin.py") + +spec = importlib.util.spec_from_file_location("client_origin", _MODULE) +client_origin = importlib.util.module_from_spec(spec) +spec.loader.exec_module(client_origin) + + +class PlatformTest(unittest.TestCase): + def test_claude(self): + self.assertEqual(client_origin._platform(), "claude") + + +class PluginVersionTest(unittest.TestCase): + def test_reads_manifest(self): + with open(client_origin._MANIFEST) as f: + expected = json.load(f)["version"] + self.assertEqual(client_origin._plugin_version(), expected) + + def test_missing_manifest(self): + with mock.patch.object(client_origin, "_MANIFEST", "/nonexistent/plugin.json"): + self.assertEqual(client_origin._plugin_version(), "unknown") + + +class HeaderTest(unittest.TestCase): + def test_format(self): + headers = client_origin.client_origin_header() + self.assertEqual(list(headers), ["X-Engram-Client"]) + platform, _, version = headers["X-Engram-Client"].partition("/") + self.assertEqual(platform, "claude-plugin") + self.assertEqual(version, client_origin._plugin_version()) + + +if __name__ == "__main__": + unittest.main()