memwal 0.1.1.dev0__tar.gz → 0.1.2.dev1__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 (22) hide show
  1. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/PKG-INFO +13 -10
  2. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/README.md +12 -9
  3. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/examples/.env.example +4 -2
  4. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/examples/async_remember_demo.py +3 -3
  5. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/examples/interactive_demo.py +3 -3
  6. memwal-0.1.2.dev1/examples/verify_credentials.py +67 -0
  7. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/__init__.py +1 -1
  8. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/client.py +21 -3
  9. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/pyproject.toml +1 -1
  10. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/run_tests.py +3 -3
  11. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/test_client.py +41 -0
  12. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/test_integration.py +5 -5
  13. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/.gitignore +0 -0
  14. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/examples/.gitignore +0 -0
  15. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/compatibility.py +0 -0
  16. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/middleware.py +0 -0
  17. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/types.py +0 -0
  18. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/memwal/utils.py +0 -0
  19. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/__init__.py +0 -0
  20. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/test_env_presets.py +0 -0
  21. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/test_middleware.py +0 -0
  22. {memwal-0.1.1.dev0 → memwal-0.1.2.dev1}/tests/test_signing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memwal
3
- Version: 0.1.1.dev0
3
+ Version: 0.1.2.dev1
4
4
  Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
5
5
  Project-URL: Homepage, https://memwal.ai
6
6
  Project-URL: Documentation, https://docs.memwal.ai
@@ -62,11 +62,14 @@ pip install memwal[all] # Everything
62
62
  Set your environment variables first:
63
63
 
64
64
  ```bash
65
- export MEMWAL_KEY="your-ed25519-delegate-key-hex"
65
+ export MEMWAL_PRIVATE_KEY="your-ed25519-delegate-private-key-hex"
66
66
  export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
67
67
  export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
68
68
  ```
69
69
 
70
+ `MEMWAL_PRIVATE_KEY` is the delegate private key from the MemWal dashboard and
71
+ must stay server-side.
72
+
70
73
  ### Async (recommended)
71
74
 
72
75
  ```python
@@ -76,7 +79,7 @@ from memwal import MemWal
76
79
 
77
80
  async def main():
78
81
  memwal = MemWal.create(
79
- key=os.environ["MEMWAL_KEY"],
82
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
80
83
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
81
84
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
82
85
  )
@@ -86,7 +89,7 @@ async def main():
86
89
  print(result.blob_id)
87
90
 
88
91
  # Recall memories
89
- matches = await memwal.recall("food allergies")
92
+ matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
90
93
  for memory in matches.results:
91
94
  print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
92
95
 
@@ -107,7 +110,7 @@ import os
107
110
  from memwal import MemWalSync
108
111
 
109
112
  client = MemWalSync.create(
110
- key=os.environ["MEMWAL_KEY"],
113
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
111
114
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
112
115
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
113
116
  )
@@ -124,7 +127,7 @@ import os
124
127
  from memwal import MemWal
125
128
 
126
129
  async with MemWal.create(
127
- key=os.environ["MEMWAL_KEY"],
130
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
128
131
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
129
132
  ) as memwal:
130
133
  await memwal.remember("I prefer dark mode")
@@ -139,7 +142,7 @@ Same shorthand as the TypeScript SDK and MCP package.
139
142
  from memwal import MemWal
140
143
 
141
144
  memwal = MemWal.create(
142
- key=os.environ["MEMWAL_KEY"],
145
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
143
146
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
144
147
  env="prod", # prod | dev | staging | local
145
148
  )
@@ -169,7 +172,7 @@ from memwal import with_memwal_langchain
169
172
  llm = ChatOpenAI(model="gpt-4o")
170
173
  smart_llm = with_memwal_langchain(
171
174
  llm,
172
- key=os.environ["MEMWAL_KEY"],
175
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
173
176
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
174
177
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
175
178
  max_memories=5,
@@ -190,7 +193,7 @@ from memwal import with_memwal_openai
190
193
  client = AsyncOpenAI()
191
194
  smart_client = with_memwal_openai(
192
195
  client,
193
- key=os.environ["MEMWAL_KEY"],
196
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
194
197
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
195
198
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
196
199
  )
@@ -213,7 +216,7 @@ Create a new async client.
213
216
  | Method | Description |
214
217
  |--------|-------------|
215
218
  | `await remember(text, namespace?)` | Store a memory |
216
- | `await recall(query, limit?, namespace?)` | Search memories |
219
+ | `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
217
220
  | `await analyze(text, namespace?)` | Extract and store facts |
218
221
  | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
219
222
  | `await restore(namespace, limit?)` | Restore a namespace |
@@ -23,11 +23,14 @@ pip install memwal[all] # Everything
23
23
  Set your environment variables first:
24
24
 
25
25
  ```bash
26
- export MEMWAL_KEY="your-ed25519-delegate-key-hex"
26
+ export MEMWAL_PRIVATE_KEY="your-ed25519-delegate-private-key-hex"
27
27
  export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
28
28
  export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
29
29
  ```
30
30
 
31
+ `MEMWAL_PRIVATE_KEY` is the delegate private key from the MemWal dashboard and
32
+ must stay server-side.
33
+
31
34
  ### Async (recommended)
32
35
 
33
36
  ```python
@@ -37,7 +40,7 @@ from memwal import MemWal
37
40
 
38
41
  async def main():
39
42
  memwal = MemWal.create(
40
- key=os.environ["MEMWAL_KEY"],
43
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
41
44
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
42
45
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
43
46
  )
@@ -47,7 +50,7 @@ async def main():
47
50
  print(result.blob_id)
48
51
 
49
52
  # Recall memories
50
- matches = await memwal.recall("food allergies")
53
+ matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
51
54
  for memory in matches.results:
52
55
  print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
53
56
 
@@ -68,7 +71,7 @@ import os
68
71
  from memwal import MemWalSync
69
72
 
70
73
  client = MemWalSync.create(
71
- key=os.environ["MEMWAL_KEY"],
74
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
72
75
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
73
76
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
74
77
  )
@@ -85,7 +88,7 @@ import os
85
88
  from memwal import MemWal
86
89
 
87
90
  async with MemWal.create(
88
- key=os.environ["MEMWAL_KEY"],
91
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
89
92
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
90
93
  ) as memwal:
91
94
  await memwal.remember("I prefer dark mode")
@@ -100,7 +103,7 @@ Same shorthand as the TypeScript SDK and MCP package.
100
103
  from memwal import MemWal
101
104
 
102
105
  memwal = MemWal.create(
103
- key=os.environ["MEMWAL_KEY"],
106
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
104
107
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
105
108
  env="prod", # prod | dev | staging | local
106
109
  )
@@ -130,7 +133,7 @@ from memwal import with_memwal_langchain
130
133
  llm = ChatOpenAI(model="gpt-4o")
131
134
  smart_llm = with_memwal_langchain(
132
135
  llm,
133
- key=os.environ["MEMWAL_KEY"],
136
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
134
137
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
135
138
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
136
139
  max_memories=5,
@@ -151,7 +154,7 @@ from memwal import with_memwal_openai
151
154
  client = AsyncOpenAI()
152
155
  smart_client = with_memwal_openai(
153
156
  client,
154
- key=os.environ["MEMWAL_KEY"],
157
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
155
158
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
156
159
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
157
160
  )
@@ -174,7 +177,7 @@ Create a new async client.
174
177
  | Method | Description |
175
178
  |--------|-------------|
176
179
  | `await remember(text, namespace?)` | Store a memory |
177
- | `await recall(query, limit?, namespace?)` | Search memories |
180
+ | `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
178
181
  | `await analyze(text, namespace?)` | Extract and store facts |
179
182
  | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
180
183
  | `await restore(namespace, limit?)` | Restore a namespace |
@@ -1,7 +1,9 @@
1
1
  # Local server (default) or remote relayer
2
- MEMWAL_SERVER_URL=http://localhost:3001
2
+ MEMWAL_SERVER_URL=http://localhost:8000
3
3
  # Ed25519 delegate private key (64-hex). Get from MemWal dashboard.
4
- MEMWAL_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
4
+ MEMWAL_PRIVATE_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
5
+ # Optional: paste the dashboard delegate public key so verification can catch mismatches.
6
+ MEMWAL_DELEGATE_PUBLIC_KEY=
5
7
  # MemWalAccount object ID on Sui (the wallet's account)
6
8
  MEMWAL_ACCOUNT_ID=0x8a1121b8f95d79e68bd07efaf71689ce6fd832b369cdb1b2a943ec7beb822392
7
9
  # Namespace for these test memories
@@ -65,13 +65,13 @@ def _ms(start: float) -> int:
65
65
 
66
66
 
67
67
  async def main() -> None:
68
- server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:3001")
69
- key = os.environ.get("MEMWAL_KEY")
68
+ server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:8000")
69
+ key = os.environ.get("MEMWAL_PRIVATE_KEY")
70
70
  account_id = os.environ.get("MEMWAL_ACCOUNT_ID")
71
71
  namespace = os.environ.get("MEMWAL_NAMESPACE", "python-sdk-example")
72
72
 
73
73
  if not key or not account_id:
74
- print("ERROR: set MEMWAL_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
74
+ print("ERROR: set MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
75
75
  sys.exit(2)
76
76
 
77
77
  print(f"server : {server_url}")
@@ -94,13 +94,13 @@ def _log_offset() -> int:
94
94
 
95
95
 
96
96
  async def main() -> None:
97
- server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:3001")
98
- key = os.environ.get("MEMWAL_KEY")
97
+ server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:8000")
98
+ key = os.environ.get("MEMWAL_PRIVATE_KEY")
99
99
  account_id = os.environ.get("MEMWAL_ACCOUNT_ID")
100
100
  namespace = os.environ.get("MEMWAL_NAMESPACE", "python-sdk-example")
101
101
 
102
102
  if not key or not account_id:
103
- print("ERROR: set MEMWAL_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
103
+ print("ERROR: set MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
104
104
  sys.exit(2)
105
105
 
106
106
  print(f"server : {server_url}")
@@ -0,0 +1,67 @@
1
+ """Verify MemWal example credentials before calling the relayer.
2
+
3
+ Usage:
4
+ MEMWAL_PRIVATE_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... python examples/verify_credentials.py
5
+
6
+ Optional:
7
+ Set MEMWAL_DELEGATE_PUBLIC_KEY to the dashboard public key to fail on
8
+ public/private key mismatch.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import re
15
+ import sys
16
+
17
+ import nacl.signing
18
+
19
+ HEX_32_BYTES = re.compile(r"^(0x)?[0-9a-fA-F]{64}$")
20
+ ACCOUNT_ID = re.compile(r"^0x[0-9a-fA-F]{64}$")
21
+
22
+
23
+ def normalize_hex(value: str) -> str:
24
+ value = value.strip()
25
+ return value[2:] if value.lower().startswith("0x") else value
26
+
27
+
28
+ def main() -> None:
29
+ private_key = os.environ.get("MEMWAL_PRIVATE_KEY") or ""
30
+ account_id = os.environ.get("MEMWAL_ACCOUNT_ID") or ""
31
+ expected_public_key = os.environ.get("MEMWAL_DELEGATE_PUBLIC_KEY") or ""
32
+ server_url = os.environ.get("MEMWAL_SERVER_URL") or ""
33
+
34
+ if not private_key:
35
+ raise SystemExit("MEMWAL_PRIVATE_KEY is required")
36
+ if not HEX_32_BYTES.match(private_key):
37
+ raise SystemExit("MEMWAL_PRIVATE_KEY must be a 64-character Ed25519 private key hex string")
38
+ if account_id and not ACCOUNT_ID.match(account_id):
39
+ raise SystemExit("MEMWAL_ACCOUNT_ID must be a 0x-prefixed 32-byte Sui object ID")
40
+
41
+ signing_key = nacl.signing.SigningKey(bytes.fromhex(normalize_hex(private_key)))
42
+ derived_public_key = signing_key.verify_key.encode().hex()
43
+
44
+ if expected_public_key and derived_public_key != normalize_hex(expected_public_key).lower():
45
+ raise SystemExit(
46
+ "MEMWAL_PRIVATE_KEY does not derive MEMWAL_DELEGATE_PUBLIC_KEY. "
47
+ "You may have pasted a public key or a key from another account."
48
+ )
49
+
50
+ print("MemWal credentials look parseable.")
51
+ print(f"Derived delegate public key: {derived_public_key}")
52
+ if account_id:
53
+ print(f"Account ID: {account_id}")
54
+ if server_url:
55
+ print(f"Relayer URL: {server_url}")
56
+ if not expected_public_key:
57
+ print("Set MEMWAL_DELEGATE_PUBLIC_KEY to fail on public/private key mismatch.")
58
+
59
+
60
+ if __name__ == "__main__":
61
+ try:
62
+ main()
63
+ except SystemExit:
64
+ raise
65
+ except Exception as exc:
66
+ print(exc, file=sys.stderr)
67
+ raise SystemExit(1) from exc
@@ -112,4 +112,4 @@ __all__ = [
112
112
  "RecallManualResult",
113
113
  ]
114
114
 
115
- __version__ = "0.1.1.dev0"
115
+ __version__ = "0.1.2.dev1"
@@ -79,6 +79,11 @@ from .utils import (
79
79
  T = TypeVar("T")
80
80
  SEAL_SESSION_TTL_MIN = 5
81
81
  SEAL_SESSION_SAFETY_MARGIN_MS = 30_000
82
+ AUTH_REJECTED_MESSAGE = (
83
+ "401 from relayer: typically wrong private key, key not registered on this "
84
+ "account, account ID mismatch, or staging/mainnet mismatch. Check .env.local "
85
+ "and dashboard credentials."
86
+ )
82
87
 
83
88
 
84
89
  # ============================================================
@@ -472,6 +477,7 @@ class MemWal:
472
477
  query: str,
473
478
  limit: int = 10,
474
479
  namespace: Optional[str] = None,
480
+ max_distance: Optional[float] = None,
475
481
  ) -> RecallResult:
476
482
  """Recall memories similar to a query.
477
483
 
@@ -481,6 +487,8 @@ class MemWal:
481
487
  query: Search query.
482
488
  limit: Max number of results (default: 10).
483
489
  namespace: Override the default namespace.
490
+ max_distance: Optional client-side relevance threshold. Memories with
491
+ ``distance >= max_distance`` are dropped.
484
492
 
485
493
  Returns:
486
494
  :class:`RecallResult` with decrypted text results.
@@ -498,6 +506,9 @@ class MemWal:
498
506
  )
499
507
  for m in data.get("results", [])
500
508
  ]
509
+ if max_distance is not None:
510
+ memories = [m for m in memories if m.distance < max_distance]
511
+ return RecallResult(results=memories, total=len(memories))
501
512
  return RecallResult(results=memories, total=data.get("total", len(memories)))
502
513
 
503
514
  async def analyze(self, text: str, namespace: Optional[str] = None) -> AnalyzeResult:
@@ -991,7 +1002,10 @@ class _HttpStatusError(MemWalError):
991
1002
  """
992
1003
 
993
1004
  def __init__(self, status: int, body: str) -> None:
994
- super().__init__(f"MemWal API error ({status}): {body}")
1005
+ if status == 401:
1006
+ super().__init__(AUTH_REJECTED_MESSAGE)
1007
+ else:
1008
+ super().__init__(f"MemWal API error ({status}): {body}")
995
1009
  self.status = status
996
1010
  self.body = body
997
1011
 
@@ -1170,10 +1184,14 @@ class MemWalSync:
1170
1184
  return self._run(self._inner.remember_bulk_and_wait(items, opts))
1171
1185
 
1172
1186
  def recall(
1173
- self, query: str, limit: int = 10, namespace: Optional[str] = None
1187
+ self,
1188
+ query: str,
1189
+ limit: int = 10,
1190
+ namespace: Optional[str] = None,
1191
+ max_distance: Optional[float] = None,
1174
1192
  ) -> RecallResult:
1175
1193
  """Synchronous version of :meth:`MemWal.recall`."""
1176
- return self._run(self._inner.recall(query, limit, namespace))
1194
+ return self._run(self._inner.recall(query, limit, namespace, max_distance))
1177
1195
 
1178
1196
  def analyze(self, text: str, namespace: Optional[str] = None) -> AnalyzeResult:
1179
1197
  """Synchronous version of :meth:`MemWal.analyze`."""
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.1.1.dev0"
7
+ version = "0.1.2.dev1"
8
8
  description = "Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -8,7 +8,7 @@ Usage:
8
8
  python3 run_tests.py
9
9
 
10
10
  With dev server (integration tests):
11
- MEMWAL_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... MEMWAL_SERVER_URL=https://... python3 run_tests.py
11
+ MEMWAL_PRIVATE_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... MEMWAL_SERVER_URL=https://... python3 run_tests.py
12
12
  """
13
13
 
14
14
  from __future__ import annotations
@@ -27,7 +27,7 @@ DIM = "\033[2m"
27
27
  RESET = "\033[0m"
28
28
 
29
29
  SERVER_URL = os.environ.get("MEMWAL_SERVER_URL", "https://relayer.dev.memwal.ai")
30
- PRIVATE_KEY = os.environ.get("MEMWAL_KEY", "")
30
+ PRIVATE_KEY = os.environ.get("MEMWAL_PRIVATE_KEY", "")
31
31
  ACCOUNT_ID = os.environ.get("MEMWAL_ACCOUNT_ID", "")
32
32
  HAS_KEY = bool(PRIVATE_KEY and ACCOUNT_ID)
33
33
 
@@ -192,7 +192,7 @@ def main() -> None:
192
192
  print(f" {'─' * 78}")
193
193
 
194
194
  if not HAS_KEY:
195
- print(f"\n {YELLOW}Integration tests skipped — set MEMWAL_KEY + MEMWAL_ACCOUNT_ID to run them{RESET}")
195
+ print(f"\n {YELLOW}Integration tests skipped — set MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID to run them{RESET}")
196
196
  print_totals(total_passed, total_failed, all_failures)
197
197
  return
198
198
 
@@ -290,6 +290,31 @@ class TestRecall:
290
290
  assert "x-seal-session" in headers
291
291
  assert "x-delegate-key" not in headers
292
292
 
293
+ @respx.mock
294
+ async def test_max_distance_filters_results(self, memwal_client: MemWal) -> None:
295
+ """recall() should filter weak matches when max_distance is provided."""
296
+ mock_seal_session_prereqs()
297
+ route = respx.post(f"{_TEST_SERVER}/api/recall").mock(
298
+ return_value=httpx.Response(
299
+ 200,
300
+ json={
301
+ "results": [
302
+ {"blob_id": "b1", "text": "I love coffee", "distance": 0.2},
303
+ {"blob_id": "b2", "text": "I live in Tokyo", "distance": 0.7},
304
+ ],
305
+ "total": 2,
306
+ },
307
+ )
308
+ )
309
+
310
+ result = await memwal_client.recall("coffee", limit=10, max_distance=0.7)
311
+
312
+ body = json.loads(route.calls[0].request.content)
313
+ assert body["limit"] == 10
314
+ assert len(result.results) == 1
315
+ assert result.total == 1
316
+ assert result.results[0].blob_id == "b1"
317
+
293
318
  @respx.mock
294
319
  async def test_get_signed_request_uses_empty_body_hash_and_no_wire_body(
295
320
  self, memwal_client: MemWal
@@ -341,6 +366,22 @@ class TestErrorHandling:
341
366
  with pytest.raises(MemWalError, match="401"):
342
367
  await memwal_client.remember("test")
343
368
 
369
+ @respx.mock
370
+ async def test_empty_401_uses_workshop_friendly_message(
371
+ self, memwal_client: MemWal
372
+ ) -> None:
373
+ """Empty-body auth failures should still give actionable guidance."""
374
+ mock_seal_session_prereqs()
375
+ respx.post(f"{_TEST_SERVER}/api/recall").mock(
376
+ return_value=httpx.Response(401, text="")
377
+ )
378
+
379
+ with pytest.raises(
380
+ MemWalError,
381
+ match="wrong private key.*account ID mismatch.*staging/mainnet mismatch",
382
+ ):
383
+ await memwal_client.recall("test")
384
+
344
385
  @respx.mock
345
386
  async def test_500_raises_memwal_error(self, memwal_client: MemWal) -> None:
346
387
  """Server errors should raise MemWalError."""
@@ -12,7 +12,7 @@ No-auth tests (always run, no env vars needed):
12
12
  - Future timestamp → 401
13
13
  - Unregistered key → SDK raises MemWalError
14
14
 
15
- Authenticated tests (require MEMWAL_KEY + MEMWAL_ACCOUNT_ID):
15
+ Authenticated tests (require MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID):
16
16
  - remember() acceptance
17
17
  - remember_and_wait()
18
18
  - recall()
@@ -26,10 +26,10 @@ Usage:
26
26
  python -m pytest tests/test_integration.py -v -m "not requires_key"
27
27
 
28
28
  # Run full suite with real credentials
29
- MEMWAL_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... python -m pytest tests/test_integration.py -v
29
+ MEMWAL_PRIVATE_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... python -m pytest tests/test_integration.py -v
30
30
 
31
31
  # Run against dev server using env vars
32
- export MEMWAL_KEY="944aa24c09d8b6d6cc6a8fbedc6dc0942a46e49db7d36596e1b6af6061ec9261"
32
+ export MEMWAL_PRIVATE_KEY="944aa24c09d8b6d6cc6a8fbedc6dc0942a46e49db7d36596e1b6af6061ec9261"
33
33
  export MEMWAL_ACCOUNT_ID="0x70f9a6ff2df0ef6a9ecbfdc3f44c27c289ec3eb0cab5e10a5c07ca6165528565"
34
34
  export MEMWAL_SERVER_URL="https://relayer.dev.memwal.ai"
35
35
  python -m pytest tests/test_integration.py -v
@@ -53,14 +53,14 @@ from memwal.utils import build_signature_message, bytes_to_hex
53
53
  # ── Config ───────────────────────────────────────────────────────────────────
54
54
 
55
55
  SERVER_URL = os.environ.get("MEMWAL_SERVER_URL", "https://relayer.dev.memwal.ai")
56
- PRIVATE_KEY_HEX = os.environ.get("MEMWAL_KEY", "")
56
+ PRIVATE_KEY_HEX = os.environ.get("MEMWAL_PRIVATE_KEY", "")
57
57
  ACCOUNT_ID = os.environ.get("MEMWAL_ACCOUNT_ID", "")
58
58
 
59
59
  HAS_KEY = bool(PRIVATE_KEY_HEX and ACCOUNT_ID)
60
60
 
61
61
  requires_key = pytest.mark.skipif(
62
62
  not HAS_KEY,
63
- reason="MEMWAL_KEY and MEMWAL_ACCOUNT_ID not set",
63
+ reason="MEMWAL_PRIVATE_KEY and MEMWAL_ACCOUNT_ID not set",
64
64
  )
65
65
 
66
66
  # ── Helpers ───────────────────────────────────────────────────────────────────
File without changes
File without changes
File without changes