memclaw-client 0.2.0__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 (29) hide show
  1. memclaw_client-0.2.0/PKG-INFO +127 -0
  2. memclaw_client-0.2.0/README.md +97 -0
  3. memclaw_client-0.2.0/pyproject.toml +66 -0
  4. memclaw_client-0.2.0/setup.cfg +4 -0
  5. memclaw_client-0.2.0/src/memclaw_client/__init__.py +20 -0
  6. memclaw_client-0.2.0/src/memclaw_client/client.py +219 -0
  7. memclaw_client-0.2.0/src/memclaw_client/exceptions.py +30 -0
  8. memclaw_client-0.2.0/src/memclaw_client/interviewer/__init__.py +10 -0
  9. memclaw_client-0.2.0/src/memclaw_client/interviewer/cli.py +328 -0
  10. memclaw_client-0.2.0/src/memclaw_client/interviewer/discovery.py +66 -0
  11. memclaw_client-0.2.0/src/memclaw_client/interviewer/machine.py +66 -0
  12. memclaw_client-0.2.0/src/memclaw_client/interviewer/parser.py +175 -0
  13. memclaw_client-0.2.0/src/memclaw_client/interviewer/runner.py +232 -0
  14. memclaw_client-0.2.0/src/memclaw_client/interviewer/scrub.py +42 -0
  15. memclaw_client-0.2.0/src/memclaw_client/interviewer/windows.py +91 -0
  16. memclaw_client-0.2.0/src/memclaw_client/models.py +56 -0
  17. memclaw_client-0.2.0/src/memclaw_client/py.typed +0 -0
  18. memclaw_client-0.2.0/src/memclaw_client.egg-info/PKG-INFO +127 -0
  19. memclaw_client-0.2.0/src/memclaw_client.egg-info/SOURCES.txt +27 -0
  20. memclaw_client-0.2.0/src/memclaw_client.egg-info/dependency_links.txt +1 -0
  21. memclaw_client-0.2.0/src/memclaw_client.egg-info/entry_points.txt +2 -0
  22. memclaw_client-0.2.0/src/memclaw_client.egg-info/requires.txt +5 -0
  23. memclaw_client-0.2.0/src/memclaw_client.egg-info/top_level.txt +1 -0
  24. memclaw_client-0.2.0/tests/test_client.py +153 -0
  25. memclaw_client-0.2.0/tests/test_interviewer_parser.py +152 -0
  26. memclaw_client-0.2.0/tests/test_interviewer_platform.py +273 -0
  27. memclaw_client-0.2.0/tests/test_interviewer_runner.py +204 -0
  28. memclaw_client-0.2.0/tests/test_interviewer_scrub.py +45 -0
  29. memclaw_client-0.2.0/tests/test_interviewer_windows.py +71 -0
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: memclaw-client
3
+ Version: 0.2.0
4
+ Summary: Official Python client for MemClaw — governed shared memory for AI agent fleets (multi-agent, multi-tenant, MCP-native).
5
+ Author-email: Caura <hello@caura.ai>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://memclaw.net
8
+ Project-URL: Documentation, https://memclaw.net/docs
9
+ Project-URL: Repository, https://github.com/caura-ai/caura-memclaw
10
+ Project-URL: Issues, https://github.com/caura-ai/caura-memclaw/issues
11
+ Keywords: memclaw,agent-memory,ai-agents,llm-memory,rag,mcp,knowledge-graph,vector-search,multi-agent
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: httpx>=0.27
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8; extra == "dev"
29
+ Requires-Dist: ruff>=0.6; extra == "dev"
30
+
31
+ # memclaw-client
32
+
33
+ Official Python client for [MemClaw](https://memclaw.net) — governed shared
34
+ memory for AI agent fleets (multi-agent, multi-tenant, MCP-native).
35
+
36
+ A thin wrapper over the MemClaw REST API. Point it at a managed
37
+ (`https://memclaw.net`) or self-hosted (`http://localhost:8000`) deployment.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install memclaw-client
43
+ ```
44
+
45
+ ## Quickstart
46
+
47
+ ```python
48
+ from memclaw_client import MemClaw
49
+
50
+ mc = MemClaw("mc_xxx", tenant_id="my-team", agent_id="my-agent")
51
+
52
+ # Write a memory — enriched server-side with type, title, tags, importance.
53
+ mc.write("Q3 revenue target is $4M, set on 2026-04-15.")
54
+
55
+ # Search (ranked raw results)
56
+ for m in mc.search("Q3 revenue target", top_k=5):
57
+ print(m.title, "—", m.content)
58
+
59
+ # Recall (LLM-synthesized context brief)
60
+ print(mc.recall("Q3 revenue target").summary)
61
+ ```
62
+
63
+ Self-hosted? Pass `base_url`:
64
+
65
+ ```python
66
+ mc = MemClaw("standalone", tenant_id="default", base_url="http://localhost:8000")
67
+ ```
68
+
69
+ ## API
70
+
71
+ | Method | Endpoint | Returns |
72
+ |---|---|---|
73
+ | `write(content, ...)` | `POST /api/v1/memories` | `Memory` |
74
+ | `search(query, top_k=5, ...)` | `POST /api/v1/search` | `list[Memory]` |
75
+ | `recall(query, top_k=5, ...)` | `POST /api/v1/recall` | `RecallResult` |
76
+ | `health()` | `GET /api/v1/health` | `dict` |
77
+
78
+ The client is a context manager (`with MemClaw(...) as mc:`) and raises
79
+ `AuthError` (401/403), `NotFoundError` (404), or `MemClawAPIError` on failures.
80
+ Every result also exposes the full API payload on `.raw`.
81
+
82
+ For credentials, scopes, and the full API surface, see the
83
+ [MemClaw docs](https://memclaw.net/docs). Production fleets should use
84
+ [per-agent keys](https://memclaw.net/docs/integrations/per-agent-keys).
85
+
86
+ ## memclaw-interviewer — Claude Code adapter (Interviewer Phase 2)
87
+
88
+ Installing this package also provides the `memclaw-interviewer` CLI: the
89
+ MemClaw Interviewer's disk-parser adapter for Claude Code workstations. It
90
+ reads Claude Code session transcripts (`~/.claude/projects/…/*.jsonl`)
91
+ **read-only**, tracks a per-file cursor via the server's forward-only
92
+ watermark documents (no local state), and submits event windows to
93
+ `POST /api/v1/interview/submit`, where MemClaw synthesizes them into typed
94
+ memories. Requires the tenant to have `interviewer.enabled = true`.
95
+
96
+ ```bash
97
+ export MEMCLAW_API_KEY=mc_xxx MEMCLAW_TENANT_ID=my-team
98
+ export MEMCLAW_INTERVIEWER_PROJECTS="-Users-me-work-*" # allowlist, default-deny
99
+
100
+ memclaw-interviewer status --since-hours 24 # cursors vs. local line counts
101
+ memclaw-interviewer run --dry-run -v # parse + window, submit nothing
102
+ memclaw-interviewer run --max-windows 8 # submit due windows
103
+ ```
104
+
105
+ **Privacy:** default-deny — with no allowlist the CLI lists discovered
106
+ project dirs and exits with guidance; `--all-projects` is the explicit
107
+ opt-in. Credential-shaped strings are scrubbed locally before anything
108
+ leaves the machine, and the server masks PII again on receipt.
109
+
110
+ **Triggers:** run it from cron, or wire Claude Code's SessionEnd hook so a
111
+ session is interviewed the moment it ends (a failed harvest never fails
112
+ the session — the hook always exits 0):
113
+
114
+ ```json
115
+ { "hooks": { "SessionEnd": [ { "hooks": [
116
+ { "type": "command", "command": "memclaw-interviewer hook", "timeout": 300 }
117
+ ] } ] } }
118
+ ```
119
+
120
+ Crash-safety is inherited from the Interviewer protocol: the watermark
121
+ advances only after the server commits a window, and retries of the same
122
+ window dedup server-side via a deterministic attempt id — never a gap,
123
+ never a duplicate.
124
+
125
+ ## License
126
+
127
+ Apache-2.0
@@ -0,0 +1,97 @@
1
+ # memclaw-client
2
+
3
+ Official Python client for [MemClaw](https://memclaw.net) — governed shared
4
+ memory for AI agent fleets (multi-agent, multi-tenant, MCP-native).
5
+
6
+ A thin wrapper over the MemClaw REST API. Point it at a managed
7
+ (`https://memclaw.net`) or self-hosted (`http://localhost:8000`) deployment.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install memclaw-client
13
+ ```
14
+
15
+ ## Quickstart
16
+
17
+ ```python
18
+ from memclaw_client import MemClaw
19
+
20
+ mc = MemClaw("mc_xxx", tenant_id="my-team", agent_id="my-agent")
21
+
22
+ # Write a memory — enriched server-side with type, title, tags, importance.
23
+ mc.write("Q3 revenue target is $4M, set on 2026-04-15.")
24
+
25
+ # Search (ranked raw results)
26
+ for m in mc.search("Q3 revenue target", top_k=5):
27
+ print(m.title, "—", m.content)
28
+
29
+ # Recall (LLM-synthesized context brief)
30
+ print(mc.recall("Q3 revenue target").summary)
31
+ ```
32
+
33
+ Self-hosted? Pass `base_url`:
34
+
35
+ ```python
36
+ mc = MemClaw("standalone", tenant_id="default", base_url="http://localhost:8000")
37
+ ```
38
+
39
+ ## API
40
+
41
+ | Method | Endpoint | Returns |
42
+ |---|---|---|
43
+ | `write(content, ...)` | `POST /api/v1/memories` | `Memory` |
44
+ | `search(query, top_k=5, ...)` | `POST /api/v1/search` | `list[Memory]` |
45
+ | `recall(query, top_k=5, ...)` | `POST /api/v1/recall` | `RecallResult` |
46
+ | `health()` | `GET /api/v1/health` | `dict` |
47
+
48
+ The client is a context manager (`with MemClaw(...) as mc:`) and raises
49
+ `AuthError` (401/403), `NotFoundError` (404), or `MemClawAPIError` on failures.
50
+ Every result also exposes the full API payload on `.raw`.
51
+
52
+ For credentials, scopes, and the full API surface, see the
53
+ [MemClaw docs](https://memclaw.net/docs). Production fleets should use
54
+ [per-agent keys](https://memclaw.net/docs/integrations/per-agent-keys).
55
+
56
+ ## memclaw-interviewer — Claude Code adapter (Interviewer Phase 2)
57
+
58
+ Installing this package also provides the `memclaw-interviewer` CLI: the
59
+ MemClaw Interviewer's disk-parser adapter for Claude Code workstations. It
60
+ reads Claude Code session transcripts (`~/.claude/projects/…/*.jsonl`)
61
+ **read-only**, tracks a per-file cursor via the server's forward-only
62
+ watermark documents (no local state), and submits event windows to
63
+ `POST /api/v1/interview/submit`, where MemClaw synthesizes them into typed
64
+ memories. Requires the tenant to have `interviewer.enabled = true`.
65
+
66
+ ```bash
67
+ export MEMCLAW_API_KEY=mc_xxx MEMCLAW_TENANT_ID=my-team
68
+ export MEMCLAW_INTERVIEWER_PROJECTS="-Users-me-work-*" # allowlist, default-deny
69
+
70
+ memclaw-interviewer status --since-hours 24 # cursors vs. local line counts
71
+ memclaw-interviewer run --dry-run -v # parse + window, submit nothing
72
+ memclaw-interviewer run --max-windows 8 # submit due windows
73
+ ```
74
+
75
+ **Privacy:** default-deny — with no allowlist the CLI lists discovered
76
+ project dirs and exits with guidance; `--all-projects` is the explicit
77
+ opt-in. Credential-shaped strings are scrubbed locally before anything
78
+ leaves the machine, and the server masks PII again on receipt.
79
+
80
+ **Triggers:** run it from cron, or wire Claude Code's SessionEnd hook so a
81
+ session is interviewed the moment it ends (a failed harvest never fails
82
+ the session — the hook always exits 0):
83
+
84
+ ```json
85
+ { "hooks": { "SessionEnd": [ { "hooks": [
86
+ { "type": "command", "command": "memclaw-interviewer hook", "timeout": 300 }
87
+ ] } ] } }
88
+ ```
89
+
90
+ Crash-safety is inherited from the Interviewer protocol: the watermark
91
+ advances only after the server commits a window, and retries of the same
92
+ window dedup server-side via a deterministic attempt id — never a gap,
93
+ never a duplicate.
94
+
95
+ ## License
96
+
97
+ Apache-2.0
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "memclaw-client"
7
+ version = "0.2.0"
8
+ description = "Official Python client for MemClaw — governed shared memory for AI agent fleets (multi-agent, multi-tenant, MCP-native)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Caura", email = "hello@caura.ai" }]
13
+ keywords = [
14
+ "memclaw",
15
+ "agent-memory",
16
+ "ai-agents",
17
+ "llm-memory",
18
+ "rag",
19
+ "mcp",
20
+ "knowledge-graph",
21
+ "vector-search",
22
+ "multi-agent",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
27
+ "License :: OSI Approved :: Apache Software License",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.9",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
35
+ "Topic :: Software Development :: Libraries :: Python Modules",
36
+ "Typing :: Typed",
37
+ ]
38
+ dependencies = ["httpx>=0.27"]
39
+
40
+ [project.scripts]
41
+ memclaw-interviewer = "memclaw_client.interviewer.cli:main"
42
+
43
+ [project.optional-dependencies]
44
+ dev = ["pytest>=8", "ruff>=0.6"]
45
+
46
+ [project.urls]
47
+ Homepage = "https://memclaw.net"
48
+ Documentation = "https://memclaw.net/docs"
49
+ Repository = "https://github.com/caura-ai/caura-memclaw"
50
+ Issues = "https://github.com/caura-ai/caura-memclaw/issues"
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.setuptools.package-data]
56
+ memclaw_client = ["py.typed"]
57
+
58
+ [tool.ruff]
59
+ target-version = "py39"
60
+ line-length = 110
61
+ src = ["src", "tests"]
62
+
63
+ [tool.pytest.ini_options]
64
+ # Self-contained config so the client suite doesn't inherit the repo-root
65
+ # pytest.ini (which configures pytest-asyncio for the backend services).
66
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,20 @@
1
+ """Official Python client for MemClaw — governed shared memory for AI agent fleets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .client import DEFAULT_BASE_URL, MemClaw
6
+ from .exceptions import AuthError, MemClawAPIError, MemClawError, NotFoundError
7
+ from .models import Memory, RecallResult
8
+
9
+ __all__ = [
10
+ "MemClaw",
11
+ "Memory",
12
+ "RecallResult",
13
+ "MemClawError",
14
+ "MemClawAPIError",
15
+ "AuthError",
16
+ "NotFoundError",
17
+ "DEFAULT_BASE_URL",
18
+ ]
19
+
20
+ __version__ = "0.1.0"
@@ -0,0 +1,219 @@
1
+ """Synchronous MemClaw client.
2
+
3
+ A thin wrapper over the MemClaw REST API. Point it at a managed
4
+ (``https://memclaw.net``) or self-hosted (``http://localhost:8000``) deployment.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import urllib.parse
10
+ from typing import Any
11
+
12
+ import httpx
13
+
14
+ from .exceptions import AuthError, MemClawAPIError, NotFoundError
15
+ from .models import Memory, RecallResult
16
+
17
+ DEFAULT_BASE_URL = "https://memclaw.net"
18
+
19
+
20
+ class MemClaw:
21
+ """Client for a MemClaw deployment.
22
+
23
+ Example::
24
+
25
+ from memclaw_client import MemClaw
26
+
27
+ mc = MemClaw("mc_xxx", tenant_id="my-team", agent_id="my-agent")
28
+ mc.write("Q3 revenue target is $4M, set on 2026-04-15.")
29
+ for m in mc.search("Q3 revenue target"):
30
+ print(m.title, m.content)
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ api_key: str,
36
+ *,
37
+ tenant_id: str,
38
+ base_url: str = DEFAULT_BASE_URL,
39
+ agent_id: str | None = None,
40
+ timeout: float = 30.0,
41
+ transport: httpx.BaseTransport | None = None,
42
+ ) -> None:
43
+ if not api_key:
44
+ raise ValueError("api_key is required")
45
+ if not tenant_id:
46
+ raise ValueError("tenant_id is required")
47
+ self.tenant_id = tenant_id
48
+ self.agent_id = agent_id
49
+ self._http = httpx.Client(
50
+ base_url=base_url.rstrip("/"),
51
+ headers={"X-API-Key": api_key, "Content-Type": "application/json"},
52
+ timeout=timeout,
53
+ transport=transport,
54
+ )
55
+
56
+ # ------------------------------------------------------------------ ops
57
+ def write(
58
+ self,
59
+ content: str,
60
+ *,
61
+ agent_id: str | None = None,
62
+ memory_type: str | None = None,
63
+ fleet_id: str | None = None,
64
+ metadata: dict[str, Any] | None = None,
65
+ **extra: Any,
66
+ ) -> Memory:
67
+ """Persist a memory. Returns the enriched ``Memory`` (POST /api/v1/memories)."""
68
+ body: dict[str, Any] = {"tenant_id": self.tenant_id, "content": content}
69
+ resolved_agent = agent_id or self.agent_id
70
+ if resolved_agent:
71
+ body["agent_id"] = resolved_agent
72
+ if memory_type:
73
+ body["memory_type"] = memory_type
74
+ if fleet_id:
75
+ body["fleet_id"] = fleet_id
76
+ if metadata is not None:
77
+ body["metadata"] = metadata
78
+ body.update(extra)
79
+ return Memory.from_dict(self._post("/api/v1/memories", body))
80
+
81
+ def search(
82
+ self,
83
+ query: str,
84
+ *,
85
+ top_k: int = 5,
86
+ fleet_ids: list[str] | None = None,
87
+ filter_agent_id: str | None = None,
88
+ **extra: Any,
89
+ ) -> list[Memory]:
90
+ """Hybrid vector + keyword search. Returns ranked ``Memory`` objects (POST /api/v1/search)."""
91
+ body: dict[str, Any] = {"tenant_id": self.tenant_id, "query": query, "top_k": top_k}
92
+ if fleet_ids:
93
+ body["fleet_ids"] = fleet_ids
94
+ if filter_agent_id:
95
+ body["filter_agent_id"] = filter_agent_id
96
+ body.update(extra)
97
+ data = self._post("/api/v1/search", body)
98
+ if not isinstance(data, dict):
99
+ raise MemClawAPIError(200, "search response must be a JSON object")
100
+ if "items" not in data:
101
+ raise MemClawAPIError(200, 'search response missing "items" list')
102
+ items = data["items"]
103
+ if not isinstance(items, list):
104
+ raise MemClawAPIError(200, 'search response "items" must be a list')
105
+ return [Memory.from_dict(m) for m in items]
106
+
107
+ def recall(self, query: str, *, top_k: int = 5, **extra: Any) -> RecallResult:
108
+ """Search + LLM summary. Returns a ``RecallResult`` context brief (POST /api/v1/recall)."""
109
+ body: dict[str, Any] = {"tenant_id": self.tenant_id, "query": query, "top_k": top_k}
110
+ body.update(extra)
111
+ return RecallResult.from_dict(self._post("/api/v1/recall", body))
112
+
113
+ def health(self) -> dict[str, Any]:
114
+ """Liveness probe (GET /api/v1/health)."""
115
+ response = self._http.get("/api/v1/health")
116
+ return response.json()
117
+
118
+ def get_document(
119
+ self,
120
+ doc_id: str,
121
+ *,
122
+ collection: str,
123
+ tenant_id: str | None = None,
124
+ ) -> dict[str, Any]:
125
+ """Fetch one structured document (GET /api/v1/documents/{doc_id}).
126
+
127
+ Returns the full ``DocOut`` envelope — the stored record is under
128
+ the ``"data"`` key. Raises ``NotFoundError`` if absent.
129
+ """
130
+ # Percent-encode the path segment: httpx does not encode f-string
131
+ # paths, so a doc_id containing '/' would hit a different route and
132
+ # '?' would inject query params.
133
+ encoded = urllib.parse.quote(doc_id, safe="")
134
+ response = self._http.get(
135
+ f"/api/v1/documents/{encoded}",
136
+ params={"tenant_id": tenant_id or self.tenant_id, "collection": collection},
137
+ )
138
+ self._raise_for_status(response)
139
+ return response.json()
140
+
141
+ def submit_interview(
142
+ self,
143
+ *,
144
+ node_id: str,
145
+ agent_id: str,
146
+ cursor_from: int,
147
+ cursor_to: int,
148
+ events: list[dict[str, Any]],
149
+ tenant_id: str | None = None,
150
+ fleet_id: str | None = None,
151
+ command_id: str | None = None,
152
+ timeout: float = 120.0,
153
+ ) -> dict[str, Any]:
154
+ """Submit one Interviewer window (POST /api/v1/interview/submit).
155
+
156
+ The server interviews the window synchronously (its budget is 90s),
157
+ so ``timeout`` defaults well above the client-wide 30s. Returns the
158
+ response body plus ``"http_status"`` so callers can distinguish a
159
+ 207 partial from a 200 committed. Raises on 4xx/5xx via the shared
160
+ error mapping (403 → ``AuthError``: tenant not enabled / bad key).
161
+ """
162
+ body: dict[str, Any] = {
163
+ "tenant_id": tenant_id or self.tenant_id,
164
+ "node_id": node_id,
165
+ "agent_id": agent_id,
166
+ "cursor_from": cursor_from,
167
+ "cursor_to": cursor_to,
168
+ "events": events,
169
+ }
170
+ if fleet_id:
171
+ body["fleet_id"] = fleet_id
172
+ if command_id:
173
+ body["command_id"] = command_id
174
+ response = self._http.post("/api/v1/interview/submit", json=body, timeout=timeout)
175
+ self._raise_for_status(response)
176
+ result = response.json()
177
+ if isinstance(result, dict):
178
+ result["http_status"] = response.status_code
179
+ return result
180
+
181
+ # ------------------------------------------------------------- internals
182
+ def _post(self, path: str, body: dict[str, Any]) -> Any:
183
+ response = self._http.post(path, json=body)
184
+ self._raise_for_status(response)
185
+ return response.json()
186
+
187
+ @staticmethod
188
+ def _raise_for_status(response: httpx.Response) -> None:
189
+ if response.is_success:
190
+ return
191
+ try:
192
+ payload: Any = response.json()
193
+ except ValueError:
194
+ payload = {}
195
+ message = ""
196
+ details: Any = None
197
+ if isinstance(payload, dict):
198
+ error = payload.get("error")
199
+ if isinstance(error, dict):
200
+ message = error.get("message") or ""
201
+ details = error.get("details")
202
+ message = message or payload.get("detail") or payload.get("message") or response.text
203
+ else:
204
+ message = response.text
205
+ if response.status_code in (401, 403):
206
+ raise AuthError(response.status_code, message or "authentication failed", details=details)
207
+ if response.status_code == 404:
208
+ raise NotFoundError(response.status_code, message or "not found", details=details)
209
+ raise MemClawAPIError(response.status_code, message or "request failed", details=details)
210
+
211
+ # ------------------------------------------------------------- lifecycle
212
+ def close(self) -> None:
213
+ self._http.close()
214
+
215
+ def __enter__(self) -> MemClaw:
216
+ return self
217
+
218
+ def __exit__(self, *exc: object) -> None:
219
+ self.close()
@@ -0,0 +1,30 @@
1
+ """Exceptions raised by the MemClaw client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class MemClawError(Exception):
9
+ """Base class for all MemClaw client errors."""
10
+
11
+
12
+ class MemClawAPIError(MemClawError):
13
+ """Raised when the MemClaw API returns a non-success status code.
14
+
15
+ The structured ``error`` envelope (``{"error": {"code", "message", "details"}}``)
16
+ is parsed when present; otherwise the raw body is used as the message.
17
+ """
18
+
19
+ def __init__(self, status_code: int, message: str, *, details: Any = None) -> None:
20
+ self.status_code = status_code
21
+ self.details = details
22
+ super().__init__(f"[{status_code}] {message}")
23
+
24
+
25
+ class AuthError(MemClawAPIError):
26
+ """Raised on 401/403 — bad or insufficiently-scoped credential."""
27
+
28
+
29
+ class NotFoundError(MemClawAPIError):
30
+ """Raised on 404."""
@@ -0,0 +1,10 @@
1
+ """memclaw-interviewer — the Claude Code disk-parser adapter (Interviewer Phase 2).
2
+
3
+ Reads Claude Code session transcripts (``~/.claude/projects/<slug>/<uuid>.jsonl``)
4
+ READ-ONLY, tracks per-file cursors via the server's forward-only watermark
5
+ documents, and submits event windows to ``POST /api/v1/interview/submit``.
6
+ No local state; no server changes; the trail belongs to Claude Code and is
7
+ never modified.
8
+ """
9
+
10
+ from __future__ import annotations