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.
- memclaw_client-0.2.0/PKG-INFO +127 -0
- memclaw_client-0.2.0/README.md +97 -0
- memclaw_client-0.2.0/pyproject.toml +66 -0
- memclaw_client-0.2.0/setup.cfg +4 -0
- memclaw_client-0.2.0/src/memclaw_client/__init__.py +20 -0
- memclaw_client-0.2.0/src/memclaw_client/client.py +219 -0
- memclaw_client-0.2.0/src/memclaw_client/exceptions.py +30 -0
- memclaw_client-0.2.0/src/memclaw_client/interviewer/__init__.py +10 -0
- memclaw_client-0.2.0/src/memclaw_client/interviewer/cli.py +328 -0
- memclaw_client-0.2.0/src/memclaw_client/interviewer/discovery.py +66 -0
- memclaw_client-0.2.0/src/memclaw_client/interviewer/machine.py +66 -0
- memclaw_client-0.2.0/src/memclaw_client/interviewer/parser.py +175 -0
- memclaw_client-0.2.0/src/memclaw_client/interviewer/runner.py +232 -0
- memclaw_client-0.2.0/src/memclaw_client/interviewer/scrub.py +42 -0
- memclaw_client-0.2.0/src/memclaw_client/interviewer/windows.py +91 -0
- memclaw_client-0.2.0/src/memclaw_client/models.py +56 -0
- memclaw_client-0.2.0/src/memclaw_client/py.typed +0 -0
- memclaw_client-0.2.0/src/memclaw_client.egg-info/PKG-INFO +127 -0
- memclaw_client-0.2.0/src/memclaw_client.egg-info/SOURCES.txt +27 -0
- memclaw_client-0.2.0/src/memclaw_client.egg-info/dependency_links.txt +1 -0
- memclaw_client-0.2.0/src/memclaw_client.egg-info/entry_points.txt +2 -0
- memclaw_client-0.2.0/src/memclaw_client.egg-info/requires.txt +5 -0
- memclaw_client-0.2.0/src/memclaw_client.egg-info/top_level.txt +1 -0
- memclaw_client-0.2.0/tests/test_client.py +153 -0
- memclaw_client-0.2.0/tests/test_interviewer_parser.py +152 -0
- memclaw_client-0.2.0/tests/test_interviewer_platform.py +273 -0
- memclaw_client-0.2.0/tests/test_interviewer_runner.py +204 -0
- memclaw_client-0.2.0/tests/test_interviewer_scrub.py +45 -0
- 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,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
|