memleaf 0.1.0__py3-none-any.whl
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.
- memleaf/__init__.py +35 -0
- memleaf/adapters/__init__.py +15 -0
- memleaf/adapters/antigravity.py +352 -0
- memleaf/adapters/base.py +746 -0
- memleaf/adapters/codex.py +460 -0
- memleaf/adapters/hermes.py +710 -0
- memleaf/budget.py +123 -0
- memleaf/capture.py +311 -0
- memleaf/cli.py +360 -0
- memleaf/compaction.py +878 -0
- memleaf/config.py +164 -0
- memleaf/frontmatter.py +403 -0
- memleaf/host_events.py +1327 -0
- memleaf/inbox.py +292 -0
- memleaf/index.py +275 -0
- memleaf/llm/__init__.py +43 -0
- memleaf/llm/base.py +349 -0
- memleaf/llm/claude_compatible.py +31 -0
- memleaf/llm/gemini.py +35 -0
- memleaf/llm/openai_compatible.py +127 -0
- memleaf/llm/router.py +172 -0
- memleaf/locking.py +134 -0
- memleaf/mcp_server.py +979 -0
- memleaf/memory_writer.py +437 -0
- memleaf/model_discovery.py +746 -0
- memleaf/models.py +285 -0
- memleaf/native_index.py +776 -0
- memleaf/processing.py +2029 -0
- memleaf/prompts.py +306 -0
- memleaf/redaction.py +41 -0
- memleaf/retrieval.py +377 -0
- memleaf/retrieval_gate.py +604 -0
- memleaf/scope_maintenance.py +480 -0
- memleaf/scope_state.py +328 -0
- memleaf/service.py +1420 -0
- memleaf/validation.py +794 -0
- memleaf/vault.py +247 -0
- memleaf-0.1.0.dist-info/METADATA +423 -0
- memleaf-0.1.0.dist-info/RECORD +43 -0
- memleaf-0.1.0.dist-info/WHEEL +5 -0
- memleaf-0.1.0.dist-info/entry_points.txt +3 -0
- memleaf-0.1.0.dist-info/licenses/LICENSE +21 -0
- memleaf-0.1.0.dist-info/top_level.txt +1 -0
memleaf/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Local-first Markdown memory core for AI agents."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .config import DEFAULT_CONFIG, default_config, load_config, save_config
|
|
6
|
+
from .frontmatter import FrontmatterError, dump_frontmatter, dump_yaml, load_yaml, parse_frontmatter
|
|
7
|
+
from .models import CaptureResult, ForgetAboutResult, Memory
|
|
8
|
+
from .redaction import redact_secrets, redact_text
|
|
9
|
+
from .retrieval import RetrievalError
|
|
10
|
+
from .service import Core, Memleaf, MemoryService
|
|
11
|
+
from .vault import Vault, safe_component
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"__version__",
|
|
15
|
+
"CaptureResult",
|
|
16
|
+
"Core",
|
|
17
|
+
"DEFAULT_CONFIG",
|
|
18
|
+
"ForgetAboutResult",
|
|
19
|
+
"FrontmatterError",
|
|
20
|
+
"Memleaf",
|
|
21
|
+
"Memory",
|
|
22
|
+
"MemoryService",
|
|
23
|
+
"RetrievalError",
|
|
24
|
+
"Vault",
|
|
25
|
+
"default_config",
|
|
26
|
+
"dump_frontmatter",
|
|
27
|
+
"dump_yaml",
|
|
28
|
+
"load_config",
|
|
29
|
+
"load_yaml",
|
|
30
|
+
"parse_frontmatter",
|
|
31
|
+
"redact_secrets",
|
|
32
|
+
"redact_text",
|
|
33
|
+
"safe_component",
|
|
34
|
+
"save_config",
|
|
35
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Host detection and MCP configuration adapters."""
|
|
2
|
+
|
|
3
|
+
from .antigravity import AntigravityAdapter
|
|
4
|
+
from .base import CommandResult, ConfigureResult, Detection
|
|
5
|
+
from .codex import CodexAdapter
|
|
6
|
+
from .hermes import HermesAdapter
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AntigravityAdapter",
|
|
10
|
+
"CodexAdapter",
|
|
11
|
+
"CommandResult",
|
|
12
|
+
"ConfigureResult",
|
|
13
|
+
"Detection",
|
|
14
|
+
"HermesAdapter",
|
|
15
|
+
]
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Antigravity central MCP JSON detection and atomic merge adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Mapping, Sequence
|
|
9
|
+
|
|
10
|
+
from .base import (
|
|
11
|
+
CommandRunner,
|
|
12
|
+
ConfigureResult,
|
|
13
|
+
Detection,
|
|
14
|
+
HookMergeResult,
|
|
15
|
+
absolute_vault,
|
|
16
|
+
adapter_environment,
|
|
17
|
+
adapter_home,
|
|
18
|
+
atomic_replace_bytes,
|
|
19
|
+
host_event_command,
|
|
20
|
+
hook_activation_status as persisted_hook_activation_status,
|
|
21
|
+
hook_definition_fingerprint,
|
|
22
|
+
make_backup,
|
|
23
|
+
merge_hook_config,
|
|
24
|
+
result_from_detection,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_ANTIGRAVITY_HOOK_USER_ACTION = (
|
|
29
|
+
"Fully quit and reopen Antigravity, then complete one test turn to activate the memleaf hooks."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AntigravityAdapter:
|
|
34
|
+
"""Handle only the existing, valid central Antigravity JSON config."""
|
|
35
|
+
|
|
36
|
+
agent = "antigravity"
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
home: Path | str | None = None,
|
|
41
|
+
env: Mapping[str, str] | None = None,
|
|
42
|
+
runner: CommandRunner | None = None,
|
|
43
|
+
*,
|
|
44
|
+
command_runner: CommandRunner | None = None,
|
|
45
|
+
path: str | Sequence[str] | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
if runner is not None and command_runner is not None:
|
|
48
|
+
raise ValueError("provide runner or command_runner, not both")
|
|
49
|
+
self.env = adapter_environment(env)
|
|
50
|
+
effective_home = home if home is not None else self.env.get("HOME")
|
|
51
|
+
self.home = adapter_home(effective_home)
|
|
52
|
+
if path is not None:
|
|
53
|
+
self.env["PATH"] = (
|
|
54
|
+
path if isinstance(path, str) else os.pathsep.join(path)
|
|
55
|
+
)
|
|
56
|
+
# Kept for a uniform injectable adapter surface; Antigravity has no
|
|
57
|
+
# supported CLI in this stage, so it is intentionally unused.
|
|
58
|
+
self.runner = runner or command_runner
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def config_path(self) -> Path:
|
|
62
|
+
return self.home / ".gemini" / "config" / "mcp_config.json"
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def hooks_path(self) -> Path:
|
|
66
|
+
return self.home / ".gemini" / "config" / "hooks.json"
|
|
67
|
+
|
|
68
|
+
def detect(self) -> Detection:
|
|
69
|
+
config = self.config_path
|
|
70
|
+
config_value = str(config)
|
|
71
|
+
if config.is_symlink():
|
|
72
|
+
return Detection(
|
|
73
|
+
agent=self.agent,
|
|
74
|
+
detected=False,
|
|
75
|
+
confidence="low",
|
|
76
|
+
reason="central configuration is a symlink; refusing access",
|
|
77
|
+
config_path=config_value,
|
|
78
|
+
status="diagnostic",
|
|
79
|
+
)
|
|
80
|
+
if not config.exists():
|
|
81
|
+
return Detection(
|
|
82
|
+
agent=self.agent,
|
|
83
|
+
detected=False,
|
|
84
|
+
confidence="none",
|
|
85
|
+
reason="central configuration was not found; no path guessed",
|
|
86
|
+
config_path=config_value,
|
|
87
|
+
status="not_detected",
|
|
88
|
+
)
|
|
89
|
+
if not config.is_file():
|
|
90
|
+
return Detection(
|
|
91
|
+
agent=self.agent,
|
|
92
|
+
detected=False,
|
|
93
|
+
confidence="low",
|
|
94
|
+
reason="central configuration is not a regular file",
|
|
95
|
+
config_path=config_value,
|
|
96
|
+
status="diagnostic",
|
|
97
|
+
)
|
|
98
|
+
state, _ = _load_config(config)
|
|
99
|
+
if state != "valid":
|
|
100
|
+
return Detection(
|
|
101
|
+
agent=self.agent,
|
|
102
|
+
detected=False,
|
|
103
|
+
confidence="low",
|
|
104
|
+
reason="central configuration is not a valid MCP JSON object",
|
|
105
|
+
config_path=config_value,
|
|
106
|
+
status="diagnostic",
|
|
107
|
+
)
|
|
108
|
+
return Detection(
|
|
109
|
+
agent=self.agent,
|
|
110
|
+
detected=True,
|
|
111
|
+
confidence="high",
|
|
112
|
+
reason="valid central MCP JSON configuration found",
|
|
113
|
+
config_path=config_value,
|
|
114
|
+
status="detected",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def configure(
|
|
118
|
+
self,
|
|
119
|
+
detection: Detection | Path | str | None = None,
|
|
120
|
+
vault: Path | str | None = None,
|
|
121
|
+
*,
|
|
122
|
+
dry_run: bool = False,
|
|
123
|
+
attempt: bool = False,
|
|
124
|
+
) -> ConfigureResult:
|
|
125
|
+
if detection is not None and not isinstance(detection, Detection) and vault is None:
|
|
126
|
+
vault = detection
|
|
127
|
+
detection = None
|
|
128
|
+
detection = self._coerce_detection(detection, vault)
|
|
129
|
+
if vault is None:
|
|
130
|
+
raise ValueError("vault is required")
|
|
131
|
+
if not detection.detected or detection.confidence != "high":
|
|
132
|
+
return result_from_detection(
|
|
133
|
+
detection,
|
|
134
|
+
status="diagnostic" if attempt else "not_detected",
|
|
135
|
+
reason=(
|
|
136
|
+
"host was not reliably detected; no configuration path guessed"
|
|
137
|
+
if attempt
|
|
138
|
+
else detection.reason
|
|
139
|
+
),
|
|
140
|
+
dry_run=dry_run,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
config = Path(detection.config_path) if detection.config_path else self.config_path
|
|
144
|
+
if config.is_symlink():
|
|
145
|
+
return result_from_detection(
|
|
146
|
+
detection,
|
|
147
|
+
status="diagnostic",
|
|
148
|
+
reason="central configuration is a symlink; unchanged",
|
|
149
|
+
dry_run=dry_run,
|
|
150
|
+
)
|
|
151
|
+
if not config.is_file():
|
|
152
|
+
return result_from_detection(
|
|
153
|
+
detection,
|
|
154
|
+
status="diagnostic",
|
|
155
|
+
reason="central configuration is not a regular file; unchanged",
|
|
156
|
+
dry_run=dry_run,
|
|
157
|
+
)
|
|
158
|
+
state, document = _load_config(config)
|
|
159
|
+
if state != "valid" or document is None:
|
|
160
|
+
return result_from_detection(
|
|
161
|
+
detection,
|
|
162
|
+
status="diagnostic",
|
|
163
|
+
reason="central configuration is invalid; unchanged",
|
|
164
|
+
dry_run=dry_run,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
hook_definition = _antigravity_hook_definition(vault)
|
|
168
|
+
hook_hash = hook_definition_fingerprint(hook_definition)
|
|
169
|
+
activation_status = persisted_hook_activation_status(
|
|
170
|
+
vault,
|
|
171
|
+
self.agent,
|
|
172
|
+
hook_hash,
|
|
173
|
+
"pending_restart",
|
|
174
|
+
)
|
|
175
|
+
if not dry_run:
|
|
176
|
+
hook_preflight = _configure_antigravity_hooks(self.hooks_path, vault, dry_run=True)
|
|
177
|
+
if hook_preflight.status == "diagnostic":
|
|
178
|
+
return result_from_detection(
|
|
179
|
+
detection,
|
|
180
|
+
status="diagnostic",
|
|
181
|
+
reason=hook_preflight.reason,
|
|
182
|
+
hook_activation_status="pending_restart",
|
|
183
|
+
hook_definition_hash=hook_hash,
|
|
184
|
+
user_action_required=True,
|
|
185
|
+
user_action=_ANTIGRAVITY_HOOK_USER_ACTION,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
expected = {
|
|
189
|
+
"command": "memleaf-mcp",
|
|
190
|
+
"args": ["--vault", absolute_vault(vault)],
|
|
191
|
+
}
|
|
192
|
+
servers = document["mcpServers"]
|
|
193
|
+
existing = servers.get("memleaf")
|
|
194
|
+
if isinstance(existing, Mapping):
|
|
195
|
+
if _entry_matches(existing, expected):
|
|
196
|
+
mcp_changed = False
|
|
197
|
+
backup = None
|
|
198
|
+
mcp_reason = "existing memleaf entry is correct"
|
|
199
|
+
else:
|
|
200
|
+
return result_from_detection(
|
|
201
|
+
detection,
|
|
202
|
+
status="diagnostic",
|
|
203
|
+
reason="existing memleaf entry is conflicting; unchanged",
|
|
204
|
+
dry_run=dry_run,
|
|
205
|
+
)
|
|
206
|
+
elif existing is not None:
|
|
207
|
+
return result_from_detection(
|
|
208
|
+
detection,
|
|
209
|
+
status="diagnostic",
|
|
210
|
+
reason="existing memleaf entry is conflicting; unchanged",
|
|
211
|
+
dry_run=dry_run,
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
if dry_run:
|
|
215
|
+
mcp_changed = False
|
|
216
|
+
backup = None
|
|
217
|
+
mcp_reason = "would atomically merge the central MCP JSON configuration"
|
|
218
|
+
else:
|
|
219
|
+
backup = None
|
|
220
|
+
try:
|
|
221
|
+
backup = make_backup(config)
|
|
222
|
+
except Exception:
|
|
223
|
+
return result_from_detection(
|
|
224
|
+
detection,
|
|
225
|
+
status="failure",
|
|
226
|
+
reason="could not create configuration backup; unchanged",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
merged = dict(document)
|
|
230
|
+
merged_servers = dict(servers)
|
|
231
|
+
merged_servers["memleaf"] = expected
|
|
232
|
+
merged["mcpServers"] = merged_servers
|
|
233
|
+
payload = (
|
|
234
|
+
json.dumps(merged, ensure_ascii=False, indent=2, separators=(",", ": "))
|
|
235
|
+
+ "\n"
|
|
236
|
+
).encode("utf-8")
|
|
237
|
+
try:
|
|
238
|
+
mode = config.stat().st_mode & 0o7777
|
|
239
|
+
atomic_replace_bytes(config, payload, mode=mode or 0o600)
|
|
240
|
+
except Exception:
|
|
241
|
+
return result_from_detection(
|
|
242
|
+
detection,
|
|
243
|
+
status="failure",
|
|
244
|
+
reason="atomic configuration update failed; backup retained",
|
|
245
|
+
backup_path=backup,
|
|
246
|
+
)
|
|
247
|
+
mcp_changed = True
|
|
248
|
+
mcp_reason = "central MCP JSON configuration updated"
|
|
249
|
+
|
|
250
|
+
hook_result = _configure_antigravity_hooks(self.hooks_path, vault, dry_run=dry_run)
|
|
251
|
+
if hook_result.status in ("diagnostic", "failure"):
|
|
252
|
+
return result_from_detection(
|
|
253
|
+
detection,
|
|
254
|
+
status=hook_result.status,
|
|
255
|
+
reason=hook_result.reason,
|
|
256
|
+
changed=mcp_changed,
|
|
257
|
+
backup_path=hook_result.backup_path or backup,
|
|
258
|
+
dry_run=dry_run,
|
|
259
|
+
hook_activation_status="pending_restart",
|
|
260
|
+
hook_definition_hash=hook_hash,
|
|
261
|
+
user_action_required=True,
|
|
262
|
+
user_action=_ANTIGRAVITY_HOOK_USER_ACTION,
|
|
263
|
+
)
|
|
264
|
+
return result_from_detection(
|
|
265
|
+
detection,
|
|
266
|
+
status=(
|
|
267
|
+
"would_configure"
|
|
268
|
+
if dry_run
|
|
269
|
+
else "configured" if mcp_changed or hook_result.changed else "already_configured"
|
|
270
|
+
),
|
|
271
|
+
reason=(
|
|
272
|
+
"would configure MCP entry and lifecycle hooks"
|
|
273
|
+
if dry_run
|
|
274
|
+
else f"{mcp_reason}; lifecycle hooks configured"
|
|
275
|
+
if mcp_changed and hook_result.changed
|
|
276
|
+
else "lifecycle hooks configured"
|
|
277
|
+
if hook_result.changed
|
|
278
|
+
else mcp_reason
|
|
279
|
+
),
|
|
280
|
+
changed=(mcp_changed or hook_result.changed) if not dry_run else False,
|
|
281
|
+
backup_path=backup or hook_result.backup_path,
|
|
282
|
+
dry_run=dry_run,
|
|
283
|
+
hook_activation_status=activation_status,
|
|
284
|
+
hook_definition_hash=hook_hash,
|
|
285
|
+
user_action_required=activation_status != "active",
|
|
286
|
+
user_action=(
|
|
287
|
+
_ANTIGRAVITY_HOOK_USER_ACTION
|
|
288
|
+
if activation_status != "active"
|
|
289
|
+
else None
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _coerce_detection(
|
|
294
|
+
self,
|
|
295
|
+
detection: Detection | Path | str | None,
|
|
296
|
+
vault: Path | str | None,
|
|
297
|
+
) -> Detection:
|
|
298
|
+
if isinstance(detection, Detection) or detection is None:
|
|
299
|
+
return detection or self.detect()
|
|
300
|
+
if vault is None:
|
|
301
|
+
return self.detect()
|
|
302
|
+
return self.detect()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
Antigravity = AntigravityAdapter
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _configure_antigravity_hooks(
|
|
309
|
+
path: Path,
|
|
310
|
+
vault: Path | str,
|
|
311
|
+
*,
|
|
312
|
+
dry_run: bool = False,
|
|
313
|
+
interpreter: str | Path | None = None,
|
|
314
|
+
) -> HookMergeResult:
|
|
315
|
+
definition = _antigravity_hook_definition(vault, interpreter=interpreter)
|
|
316
|
+
return merge_hook_config(
|
|
317
|
+
path,
|
|
318
|
+
definition,
|
|
319
|
+
container_key="memleaf",
|
|
320
|
+
dry_run=dry_run,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _antigravity_hook_definition(
|
|
325
|
+
vault: Path | str,
|
|
326
|
+
*,
|
|
327
|
+
interpreter: str | Path | None = None,
|
|
328
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
329
|
+
command = host_event_command("antigravity", "PreInvocation", vault, interpreter=interpreter)
|
|
330
|
+
stop_command = host_event_command("antigravity", "Stop", vault, interpreter=interpreter)
|
|
331
|
+
return {
|
|
332
|
+
"PreInvocation": [{"type": "command", "command": command, "timeout": 600}],
|
|
333
|
+
"Stop": [{"type": "command", "command": stop_command, "timeout": 600}],
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _load_config(path: Path) -> tuple[str, dict[str, Any] | None]:
|
|
338
|
+
try:
|
|
339
|
+
with path.open("r", encoding="utf-8") as stream:
|
|
340
|
+
value = json.load(stream)
|
|
341
|
+
except (OSError, UnicodeError, ValueError):
|
|
342
|
+
return "invalid", None
|
|
343
|
+
if not isinstance(value, dict) or not isinstance(value.get("mcpServers"), dict):
|
|
344
|
+
return "invalid", None
|
|
345
|
+
return "valid", value
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _entry_matches(entry: Mapping[str, Any], expected: Mapping[str, Any]) -> bool:
|
|
349
|
+
return (
|
|
350
|
+
entry.get("command") == expected.get("command")
|
|
351
|
+
and entry.get("args") == expected.get("args")
|
|
352
|
+
)
|