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
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""Codex CLI detection and conservative MCP configuration."""
|
|
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
|
+
try:
|
|
11
|
+
import pwd
|
|
12
|
+
except ImportError: # pragma: no cover - Windows has no POSIX passwd DB.
|
|
13
|
+
pwd = None
|
|
14
|
+
|
|
15
|
+
from .base import (
|
|
16
|
+
CommandRunner,
|
|
17
|
+
ConfigureResult,
|
|
18
|
+
Detection,
|
|
19
|
+
HookMergeResult,
|
|
20
|
+
adapter_environment,
|
|
21
|
+
adapter_home,
|
|
22
|
+
absolute_vault,
|
|
23
|
+
command_is_missing,
|
|
24
|
+
host_event_command,
|
|
25
|
+
hook_activation_status as persisted_hook_activation_status,
|
|
26
|
+
hook_definition_fingerprint,
|
|
27
|
+
make_backup,
|
|
28
|
+
merge_hook_config,
|
|
29
|
+
mcp_command,
|
|
30
|
+
resolve_executable,
|
|
31
|
+
result_from_detection,
|
|
32
|
+
run_argv,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
CODEX_EXECUTABLE = Path("/Applications/ChatGPT.app/Contents/Resources/codex")
|
|
37
|
+
_CODEX_HOOK_TRUST_STATUS = "pending_user_review"
|
|
38
|
+
_CODEX_HOOK_USER_ACTION = "Open Codex and run /hooks to review and trust the memleaf hooks."
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CodexAdapter:
|
|
42
|
+
"""Use the official Codex CLI for user-level MCP configuration.
|
|
43
|
+
|
|
44
|
+
The adapter never edits TOML itself. It asks ``codex mcp get`` what is
|
|
45
|
+
already configured, and only then invokes the documented ``mcp add``
|
|
46
|
+
command for a missing entry.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
agent = "codex"
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
home: Path | str | None = None,
|
|
54
|
+
env: Mapping[str, str] | None = None,
|
|
55
|
+
runner: CommandRunner | None = None,
|
|
56
|
+
*,
|
|
57
|
+
command_runner: CommandRunner | None = None,
|
|
58
|
+
path: str | Sequence[str] | None = None,
|
|
59
|
+
known_paths: Sequence[Path | str] | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
if runner is not None and command_runner is not None:
|
|
62
|
+
raise ValueError("provide runner or command_runner, not both")
|
|
63
|
+
home_was_explicit = home is not None or (env is not None and "HOME" in env)
|
|
64
|
+
self.env = adapter_environment(env)
|
|
65
|
+
effective_home = home if home is not None else self.env.get("HOME")
|
|
66
|
+
self.home = adapter_home(effective_home)
|
|
67
|
+
if known_paths is None:
|
|
68
|
+
# An explicit home marks an injected/sandbox boundary. Do not let
|
|
69
|
+
# a system installation escape that boundary when init is tested
|
|
70
|
+
# with an isolated HOME. The default adapter may still inspect
|
|
71
|
+
# the documented application path.
|
|
72
|
+
try:
|
|
73
|
+
if pwd is None:
|
|
74
|
+
raise OSError
|
|
75
|
+
login_home = Path(pwd.getpwuid(os.getuid()).pw_dir).resolve()
|
|
76
|
+
except (KeyError, OSError, RuntimeError):
|
|
77
|
+
login_home = None
|
|
78
|
+
self.known_paths = (
|
|
79
|
+
(CODEX_EXECUTABLE,)
|
|
80
|
+
if not home_was_explicit or self.home == login_home
|
|
81
|
+
else ()
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
self.known_paths = tuple(known_paths)
|
|
85
|
+
if path is not None:
|
|
86
|
+
self.env["PATH"] = (
|
|
87
|
+
path if isinstance(path, str) else os.pathsep.join(path)
|
|
88
|
+
)
|
|
89
|
+
self.runner = runner or command_runner
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def config_path(self) -> Path:
|
|
93
|
+
return self.home / ".codex" / "config.toml"
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def hooks_path(self) -> Path:
|
|
97
|
+
return self.home / ".codex" / "hooks.json"
|
|
98
|
+
|
|
99
|
+
def detect(self) -> Detection:
|
|
100
|
+
config = self.config_path
|
|
101
|
+
executable = resolve_executable(
|
|
102
|
+
"codex", env=self.env, known_paths=self.known_paths
|
|
103
|
+
)
|
|
104
|
+
config_value = str(config)
|
|
105
|
+
if executable is not None:
|
|
106
|
+
return Detection(
|
|
107
|
+
agent=self.agent,
|
|
108
|
+
detected=True,
|
|
109
|
+
confidence="high",
|
|
110
|
+
reason="executable found",
|
|
111
|
+
executable=executable,
|
|
112
|
+
config_path=config_value,
|
|
113
|
+
status="detected",
|
|
114
|
+
)
|
|
115
|
+
if config.is_file() and not config.is_symlink():
|
|
116
|
+
return Detection(
|
|
117
|
+
agent=self.agent,
|
|
118
|
+
detected=True,
|
|
119
|
+
confidence="medium",
|
|
120
|
+
reason="user configuration found but executable is unavailable",
|
|
121
|
+
executable=None,
|
|
122
|
+
config_path=config_value,
|
|
123
|
+
status="diagnostic",
|
|
124
|
+
)
|
|
125
|
+
if config.is_symlink():
|
|
126
|
+
return Detection(
|
|
127
|
+
agent=self.agent,
|
|
128
|
+
detected=False,
|
|
129
|
+
confidence="low",
|
|
130
|
+
reason="configuration path is a symlink; refusing automatic access",
|
|
131
|
+
executable=None,
|
|
132
|
+
config_path=config_value,
|
|
133
|
+
status="diagnostic",
|
|
134
|
+
)
|
|
135
|
+
return Detection(
|
|
136
|
+
agent=self.agent,
|
|
137
|
+
detected=False,
|
|
138
|
+
confidence="none",
|
|
139
|
+
reason="executable and user configuration were not found",
|
|
140
|
+
executable=None,
|
|
141
|
+
config_path=config_value,
|
|
142
|
+
status="not_detected",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def configure(
|
|
146
|
+
self,
|
|
147
|
+
detection: Detection | Path | str | None = None,
|
|
148
|
+
vault: Path | str | None = None,
|
|
149
|
+
*,
|
|
150
|
+
dry_run: bool = False,
|
|
151
|
+
attempt: bool = False,
|
|
152
|
+
) -> ConfigureResult:
|
|
153
|
+
if detection is not None and not isinstance(detection, Detection) and vault is None:
|
|
154
|
+
vault = detection
|
|
155
|
+
detection = None
|
|
156
|
+
detection = self._coerce_detection(detection, vault)
|
|
157
|
+
if vault is None:
|
|
158
|
+
raise ValueError("vault is required")
|
|
159
|
+
if not detection.detected or detection.confidence != "high":
|
|
160
|
+
return result_from_detection(
|
|
161
|
+
detection,
|
|
162
|
+
status="diagnostic" if attempt else "not_detected",
|
|
163
|
+
reason=(
|
|
164
|
+
"host was not reliably detected; no executable path guessed"
|
|
165
|
+
if attempt
|
|
166
|
+
else detection.reason
|
|
167
|
+
),
|
|
168
|
+
dry_run=dry_run,
|
|
169
|
+
)
|
|
170
|
+
if not detection.executable:
|
|
171
|
+
return result_from_detection(
|
|
172
|
+
detection,
|
|
173
|
+
status="diagnostic",
|
|
174
|
+
reason="detected host has no executable path",
|
|
175
|
+
dry_run=dry_run,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
executable = detection.executable
|
|
179
|
+
get_command = [executable, "mcp", "get", "memleaf", "--json"]
|
|
180
|
+
add_command = [
|
|
181
|
+
executable,
|
|
182
|
+
"mcp",
|
|
183
|
+
"add",
|
|
184
|
+
"memleaf",
|
|
185
|
+
"--",
|
|
186
|
+
*mcp_command(vault),
|
|
187
|
+
]
|
|
188
|
+
hook_definition = _codex_hook_definition(vault)
|
|
189
|
+
hook_hash = hook_definition_fingerprint(hook_definition)
|
|
190
|
+
activation_status = persisted_hook_activation_status(
|
|
191
|
+
vault,
|
|
192
|
+
self.agent,
|
|
193
|
+
hook_hash,
|
|
194
|
+
_CODEX_HOOK_TRUST_STATUS,
|
|
195
|
+
)
|
|
196
|
+
hook_trust_status = "trusted" if activation_status == "active" else _CODEX_HOOK_TRUST_STATUS
|
|
197
|
+
if dry_run:
|
|
198
|
+
hook_result = _configure_codex_hooks(self.hooks_path, vault, dry_run=True)
|
|
199
|
+
if hook_result.status == "diagnostic":
|
|
200
|
+
return result_from_detection(
|
|
201
|
+
detection,
|
|
202
|
+
status="diagnostic",
|
|
203
|
+
reason=hook_result.reason,
|
|
204
|
+
command=add_command,
|
|
205
|
+
dry_run=True,
|
|
206
|
+
hook_trust_status=_CODEX_HOOK_TRUST_STATUS,
|
|
207
|
+
hook_activation_status=_CODEX_HOOK_TRUST_STATUS,
|
|
208
|
+
hook_definition_hash=hook_hash,
|
|
209
|
+
user_action_required=True,
|
|
210
|
+
user_action=_CODEX_HOOK_USER_ACTION,
|
|
211
|
+
)
|
|
212
|
+
return result_from_detection(
|
|
213
|
+
detection,
|
|
214
|
+
status="would_configure",
|
|
215
|
+
reason="would configure MCP entry and lifecycle hooks",
|
|
216
|
+
command=add_command,
|
|
217
|
+
dry_run=True,
|
|
218
|
+
hook_trust_status=hook_trust_status,
|
|
219
|
+
hook_activation_status=activation_status,
|
|
220
|
+
hook_definition_hash=hook_hash,
|
|
221
|
+
user_action_required=activation_status != "active",
|
|
222
|
+
user_action=_CODEX_HOOK_USER_ACTION if activation_status != "active" else None,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
hook_preflight = _configure_codex_hooks(self.hooks_path, vault, dry_run=True)
|
|
226
|
+
if hook_preflight.status == "diagnostic":
|
|
227
|
+
return result_from_detection(
|
|
228
|
+
detection,
|
|
229
|
+
status="diagnostic",
|
|
230
|
+
reason=hook_preflight.reason,
|
|
231
|
+
command=get_command,
|
|
232
|
+
hook_trust_status=_CODEX_HOOK_TRUST_STATUS,
|
|
233
|
+
hook_activation_status=_CODEX_HOOK_TRUST_STATUS,
|
|
234
|
+
hook_definition_hash=hook_hash,
|
|
235
|
+
user_action_required=True,
|
|
236
|
+
user_action=_CODEX_HOOK_USER_ACTION,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
config = Path(detection.config_path) if detection.config_path else self.config_path
|
|
240
|
+
if config.is_symlink():
|
|
241
|
+
return result_from_detection(
|
|
242
|
+
detection,
|
|
243
|
+
status="diagnostic",
|
|
244
|
+
reason="configuration path is a symlink; unchanged",
|
|
245
|
+
command=get_command,
|
|
246
|
+
)
|
|
247
|
+
if config.exists() and not config.is_file():
|
|
248
|
+
return result_from_detection(
|
|
249
|
+
detection,
|
|
250
|
+
status="diagnostic",
|
|
251
|
+
reason="configuration path is not a regular file; unchanged",
|
|
252
|
+
command=get_command,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
queried = run_argv(self.runner, get_command, env=self.env)
|
|
257
|
+
except Exception:
|
|
258
|
+
return result_from_detection(
|
|
259
|
+
detection,
|
|
260
|
+
status="failure",
|
|
261
|
+
reason="could not query existing MCP entry",
|
|
262
|
+
command=get_command,
|
|
263
|
+
)
|
|
264
|
+
if queried.returncode == 0:
|
|
265
|
+
entry = _entry_from_json(queried.stdout)
|
|
266
|
+
if entry is not None and _entry_matches(entry, vault):
|
|
267
|
+
mcp_changed = False
|
|
268
|
+
backup = None
|
|
269
|
+
mcp_reason = "existing memleaf entry is correct"
|
|
270
|
+
else:
|
|
271
|
+
return result_from_detection(
|
|
272
|
+
detection,
|
|
273
|
+
status="diagnostic",
|
|
274
|
+
reason="existing memleaf entry is unknown or conflicting; unchanged",
|
|
275
|
+
command=get_command,
|
|
276
|
+
)
|
|
277
|
+
elif command_is_missing(queried):
|
|
278
|
+
backup = None
|
|
279
|
+
try:
|
|
280
|
+
backup = make_backup(config)
|
|
281
|
+
except Exception:
|
|
282
|
+
return result_from_detection(
|
|
283
|
+
detection,
|
|
284
|
+
status="failure",
|
|
285
|
+
reason="could not create configuration backup; unchanged",
|
|
286
|
+
command=add_command,
|
|
287
|
+
)
|
|
288
|
+
try:
|
|
289
|
+
added = run_argv(self.runner, add_command, env=self.env)
|
|
290
|
+
except Exception:
|
|
291
|
+
return result_from_detection(
|
|
292
|
+
detection,
|
|
293
|
+
status="failure",
|
|
294
|
+
reason="official CLI add failed; backup retained",
|
|
295
|
+
backup_path=backup,
|
|
296
|
+
command=add_command,
|
|
297
|
+
)
|
|
298
|
+
if added.returncode != 0:
|
|
299
|
+
return result_from_detection(
|
|
300
|
+
detection,
|
|
301
|
+
status="failure",
|
|
302
|
+
reason="official CLI add failed; backup retained",
|
|
303
|
+
backup_path=backup,
|
|
304
|
+
command=add_command,
|
|
305
|
+
)
|
|
306
|
+
mcp_changed = True
|
|
307
|
+
mcp_reason = "MCP entry added by official CLI"
|
|
308
|
+
else:
|
|
309
|
+
return result_from_detection(
|
|
310
|
+
detection,
|
|
311
|
+
status="failure",
|
|
312
|
+
reason="could not establish whether memleaf is already configured",
|
|
313
|
+
command=get_command,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
hook_result = _configure_codex_hooks(self.hooks_path, vault)
|
|
317
|
+
if hook_result.status in ("diagnostic", "failure"):
|
|
318
|
+
return result_from_detection(
|
|
319
|
+
detection,
|
|
320
|
+
status=hook_result.status,
|
|
321
|
+
reason=hook_result.reason,
|
|
322
|
+
changed=mcp_changed,
|
|
323
|
+
backup_path=hook_result.backup_path or backup,
|
|
324
|
+
command=add_command,
|
|
325
|
+
hook_trust_status=_CODEX_HOOK_TRUST_STATUS,
|
|
326
|
+
hook_activation_status=_CODEX_HOOK_TRUST_STATUS,
|
|
327
|
+
hook_definition_hash=hook_hash,
|
|
328
|
+
user_action_required=True,
|
|
329
|
+
user_action=_CODEX_HOOK_USER_ACTION,
|
|
330
|
+
)
|
|
331
|
+
return result_from_detection(
|
|
332
|
+
detection,
|
|
333
|
+
# Preserve the adapter's historical status for an existing MCP
|
|
334
|
+
# entry; ``changed`` still reports that lifecycle hooks were added.
|
|
335
|
+
status="configured" if mcp_changed else "already_configured",
|
|
336
|
+
reason=(
|
|
337
|
+
f"{mcp_reason}; lifecycle hooks configured"
|
|
338
|
+
if mcp_changed and hook_result.changed
|
|
339
|
+
else "lifecycle hooks configured"
|
|
340
|
+
if hook_result.changed
|
|
341
|
+
else mcp_reason
|
|
342
|
+
),
|
|
343
|
+
changed=mcp_changed or hook_result.changed,
|
|
344
|
+
backup_path=backup or hook_result.backup_path,
|
|
345
|
+
command=add_command,
|
|
346
|
+
hook_trust_status=hook_trust_status,
|
|
347
|
+
hook_activation_status=activation_status,
|
|
348
|
+
hook_definition_hash=hook_hash,
|
|
349
|
+
user_action_required=activation_status != "active",
|
|
350
|
+
user_action=_CODEX_HOOK_USER_ACTION if activation_status != "active" else None,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def _coerce_detection(
|
|
354
|
+
self,
|
|
355
|
+
detection: Detection | Path | str | None,
|
|
356
|
+
vault: Path | str | None,
|
|
357
|
+
) -> Detection:
|
|
358
|
+
if isinstance(detection, Detection) or detection is None:
|
|
359
|
+
return detection or self.detect()
|
|
360
|
+
if vault is None:
|
|
361
|
+
return self.detect()
|
|
362
|
+
return self.detect()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
Codex = CodexAdapter
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _configure_codex_hooks(
|
|
369
|
+
path: Path,
|
|
370
|
+
vault: Path | str,
|
|
371
|
+
*,
|
|
372
|
+
dry_run: bool = False,
|
|
373
|
+
interpreter: str | Path | None = None,
|
|
374
|
+
) -> HookMergeResult:
|
|
375
|
+
definition = _codex_hook_definition(vault, interpreter=interpreter)
|
|
376
|
+
return merge_hook_config(
|
|
377
|
+
path,
|
|
378
|
+
definition,
|
|
379
|
+
container_key="hooks",
|
|
380
|
+
dry_run=dry_run,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _codex_hook_definition(
|
|
385
|
+
vault: Path | str,
|
|
386
|
+
*,
|
|
387
|
+
interpreter: str | Path | None = None,
|
|
388
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
389
|
+
command = host_event_command("codex", "UserPromptSubmit", vault, interpreter=interpreter)
|
|
390
|
+
pre_tool_command = host_event_command("codex", "PreToolUse", vault, interpreter=interpreter)
|
|
391
|
+
post_tool_command = host_event_command("codex", "PostToolUse", vault, interpreter=interpreter)
|
|
392
|
+
stop_command = host_event_command("codex", "Stop", vault, interpreter=interpreter)
|
|
393
|
+
return {
|
|
394
|
+
"UserPromptSubmit": [{"hooks": [{"type": "command", "command": command, "timeout": 600}]}],
|
|
395
|
+
"PreToolUse": [
|
|
396
|
+
{
|
|
397
|
+
"matcher": r"^mcp__memleaf__(search|read)$",
|
|
398
|
+
"hooks": [{"type": "command", "command": pre_tool_command, "timeout": 30}],
|
|
399
|
+
}
|
|
400
|
+
],
|
|
401
|
+
"PostToolUse": [
|
|
402
|
+
{
|
|
403
|
+
"matcher": r"^mcp__memleaf__search$",
|
|
404
|
+
"hooks": [{"type": "command", "command": post_tool_command, "timeout": 30}],
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
"Stop": [{"hooks": [{"type": "command", "command": stop_command, "timeout": 600}]}],
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _entry_from_json(text: str) -> dict[str, Any] | None:
|
|
412
|
+
try:
|
|
413
|
+
value = json.loads(text)
|
|
414
|
+
except (TypeError, ValueError):
|
|
415
|
+
return None
|
|
416
|
+
return _find_named_entry(value, "memleaf")
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _find_named_entry(value: Any, name: str) -> dict[str, Any] | None:
|
|
420
|
+
if isinstance(value, Mapping):
|
|
421
|
+
if "command" in value and "args" in value:
|
|
422
|
+
return dict(value)
|
|
423
|
+
direct = value.get(name)
|
|
424
|
+
if isinstance(direct, Mapping):
|
|
425
|
+
return dict(direct)
|
|
426
|
+
if value.get("name") == name:
|
|
427
|
+
return dict(value)
|
|
428
|
+
for key in ("mcpServers", "mcp_servers", "servers", "server", "mcp"):
|
|
429
|
+
nested = value.get(key)
|
|
430
|
+
if isinstance(nested, Mapping):
|
|
431
|
+
found = _find_named_entry(nested, name)
|
|
432
|
+
if found is not None:
|
|
433
|
+
return found
|
|
434
|
+
for nested in value.values():
|
|
435
|
+
if isinstance(nested, Mapping):
|
|
436
|
+
if nested.get("name") == name:
|
|
437
|
+
return dict(nested)
|
|
438
|
+
if "command" in nested and "args" in nested:
|
|
439
|
+
return dict(nested)
|
|
440
|
+
elif isinstance(value, list):
|
|
441
|
+
for nested in value:
|
|
442
|
+
found = _find_named_entry(nested, name)
|
|
443
|
+
if found is not None:
|
|
444
|
+
return found
|
|
445
|
+
return None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _entry_matches(entry: Mapping[str, Any], vault: Path | str) -> bool:
|
|
449
|
+
candidate: Mapping[str, Any] = entry
|
|
450
|
+
transport = entry.get("transport")
|
|
451
|
+
if transport is not None:
|
|
452
|
+
if not isinstance(transport, Mapping) or transport.get("type") != "stdio":
|
|
453
|
+
return False
|
|
454
|
+
candidate = transport
|
|
455
|
+
elif candidate.get("type") not in (None, "stdio"):
|
|
456
|
+
return False
|
|
457
|
+
return candidate.get("command") == "memleaf-mcp" and candidate.get("args") == [
|
|
458
|
+
"--vault",
|
|
459
|
+
str(Path(vault).expanduser().resolve()),
|
|
460
|
+
]
|