memkernel-mcp 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.
- memkernel/__init__.py +33 -0
- memkernel/cli.py +409 -0
- memkernel/clients/__init__.py +1 -0
- memkernel/clients/adapter.py +296 -0
- memkernel/core/__init__.py +1 -0
- memkernel/core/branch.py +161 -0
- memkernel/core/events.py +209 -0
- memkernel/core/locks.py +188 -0
- memkernel/core/store.py +643 -0
- memkernel/kernel.py +352 -0
- memkernel/memory/__init__.py +1 -0
- memkernel/memory/gc.py +370 -0
- memkernel/memory/state_view.py +412 -0
- memkernel/memory/token_counter.py +67 -0
- memkernel/server/__init__.py +1 -0
- memkernel/server/mcp_server.py +379 -0
- memkernel_mcp-0.1.0.dist-info/METADATA +265 -0
- memkernel_mcp-0.1.0.dist-info/RECORD +22 -0
- memkernel_mcp-0.1.0.dist-info/WHEEL +5 -0
- memkernel_mcp-0.1.0.dist-info/entry_points.txt +3 -0
- memkernel_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
- memkernel_mcp-0.1.0.dist-info/top_level.txt +1 -0
memkernel/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""MemKernel: an event-sourced memory kernel for multi-agent coding systems."""
|
|
2
|
+
|
|
3
|
+
from memkernel.core.events import EventEnvelope, EventType
|
|
4
|
+
from memkernel.core.locks import LockManager
|
|
5
|
+
from memkernel.core.store import AsyncEventStore, BranchExistsError, BranchNotFoundError, StaleHeadError
|
|
6
|
+
from memkernel.kernel import DEFAULT_BRANCH, FencingTokenError, LeaseGrant, MemKernel
|
|
7
|
+
from memkernel.memory.gc import GCConfig, GCResult, HeuristicSummarizer, LLMSummarizer, MemoryGCEngine
|
|
8
|
+
from memkernel.memory.state_view import WorkspaceState
|
|
9
|
+
from memkernel.memory.token_counter import TokenCounter
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AsyncEventStore",
|
|
15
|
+
"BranchExistsError",
|
|
16
|
+
"BranchNotFoundError",
|
|
17
|
+
"DEFAULT_BRANCH",
|
|
18
|
+
"EventEnvelope",
|
|
19
|
+
"EventType",
|
|
20
|
+
"FencingTokenError",
|
|
21
|
+
"GCConfig",
|
|
22
|
+
"GCResult",
|
|
23
|
+
"HeuristicSummarizer",
|
|
24
|
+
"LLMSummarizer",
|
|
25
|
+
"LeaseGrant",
|
|
26
|
+
"LockManager",
|
|
27
|
+
"MemKernel",
|
|
28
|
+
"MemoryGCEngine",
|
|
29
|
+
"StaleHeadError",
|
|
30
|
+
"TokenCounter",
|
|
31
|
+
"WorkspaceState",
|
|
32
|
+
"__version__",
|
|
33
|
+
]
|
memkernel/cli.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""``memkernel`` command line: attach agents, check health, inspect memory.
|
|
2
|
+
|
|
3
|
+
memkernel attach claude-code # or cursor | codex | antigravity | all
|
|
4
|
+
memkernel doctor # end-to-end health check
|
|
5
|
+
memkernel status # what the agents currently remember
|
|
6
|
+
memkernel serve # run the MCP server (what agents launch)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import tomllib
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from importlib import metadata
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable
|
|
22
|
+
|
|
23
|
+
from memkernel.kernel import DEFAULT_BRANCH, MemKernel, default_db_path
|
|
24
|
+
|
|
25
|
+
SERVER_NAME = "memkernel"
|
|
26
|
+
SERVER_MODULE = "memkernel.server.mcp_server"
|
|
27
|
+
CODEX_STARTUP_TIMEOUT_SEC = 120
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------ attach
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Client:
|
|
35
|
+
name: str
|
|
36
|
+
agent_id: str
|
|
37
|
+
project_scoped: bool
|
|
38
|
+
config_paths: Callable[[Path, Path], list[Path]] # (project, home) -> files to write
|
|
39
|
+
fmt: str # "json" | "toml"
|
|
40
|
+
hint: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _antigravity_paths(project: Path, home: Path) -> list[Path]:
|
|
44
|
+
# Antigravity builds have used several locations; update every one that exists.
|
|
45
|
+
candidates = [
|
|
46
|
+
home / ".gemini" / "antigravity" / "mcp_config.json",
|
|
47
|
+
home / ".gemini" / "antigravity-ide" / "mcp_config.json",
|
|
48
|
+
home / ".gemini" / "config" / "mcp_config.json",
|
|
49
|
+
]
|
|
50
|
+
existing = [p for p in candidates if p.exists()]
|
|
51
|
+
return existing or candidates[:1]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
CLIENTS: dict[str, Client] = {
|
|
55
|
+
"claude-code": Client(
|
|
56
|
+
"claude-code", "claude-code-cli", True, lambda p, h: [p / ".mcp.json"], "json",
|
|
57
|
+
"Restart Claude Code in this project and approve the 'memkernel' server when prompted (/mcp lists it). "
|
|
58
|
+
"On a slow machine, raise its 30 s server start limit: set MCP_TIMEOUT=120000 before launching claude.",
|
|
59
|
+
),
|
|
60
|
+
"cursor": Client(
|
|
61
|
+
"cursor", "cursor-ide", True, lambda p, h: [p / ".cursor" / "mcp.json"], "json",
|
|
62
|
+
"Open Cursor Settings -> MCP and enable 'memkernel'.",
|
|
63
|
+
),
|
|
64
|
+
"codex": Client(
|
|
65
|
+
"codex", "codex-cli", False, lambda p, h: [h / ".codex" / "config.toml"], "toml",
|
|
66
|
+
"Start a new Codex session; `codex mcp list` shows 'memkernel'.",
|
|
67
|
+
),
|
|
68
|
+
"antigravity": Client(
|
|
69
|
+
"antigravity", "antigravity", False, _antigravity_paths, "json",
|
|
70
|
+
"Reload MCP servers in Antigravity (Manage MCP Servers -> Refresh).",
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AttachError(RuntimeError):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def server_launch(*, portable: bool) -> tuple[str, list[str]]:
|
|
80
|
+
"""Command that starts the server. Absolute interpreter by default: GUI
|
|
81
|
+
apps often do not inherit the shell PATH. ``portable`` uses the
|
|
82
|
+
``memkernel`` entry point on PATH (for configs committed to a repo)."""
|
|
83
|
+
if portable:
|
|
84
|
+
return "memkernel", ["serve"]
|
|
85
|
+
return sys.executable, ["-m", SERVER_MODULE]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_entry(client: Client, *, project: Path, db: Path | None, portable: bool, agent_id: str | None) -> dict[str, Any]:
|
|
89
|
+
command, args = server_launch(portable=portable)
|
|
90
|
+
env = {"MEMKERNEL_AGENT_ID": agent_id or client.agent_id}
|
|
91
|
+
if db is not None:
|
|
92
|
+
env["MEMKERNEL_DB"] = str(db)
|
|
93
|
+
elif client.project_scoped and not portable:
|
|
94
|
+
env["MEMKERNEL_DB"] = str(project / ".memkernel" / "memkernel.db")
|
|
95
|
+
return {"command": command, "args": args, "env": env}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _atomic_write(path: Path, text: str) -> None:
|
|
99
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
tmp = path.with_name(path.name + ".memkernel-tmp")
|
|
101
|
+
tmp.write_text(text, encoding="utf-8")
|
|
102
|
+
os.replace(tmp, path)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def merge_json_config(path: Path, entry: dict[str, Any]) -> tuple[str, str]:
|
|
106
|
+
"""Return ``(action, new_text)``; other servers in the file are preserved."""
|
|
107
|
+
data: dict[str, Any] = {}
|
|
108
|
+
if path.exists():
|
|
109
|
+
raw = path.read_text(encoding="utf-8")
|
|
110
|
+
if raw.strip():
|
|
111
|
+
try:
|
|
112
|
+
data = json.loads(raw)
|
|
113
|
+
except json.JSONDecodeError as exc:
|
|
114
|
+
raise AttachError(f"{path} is not valid JSON ({exc}); fix or remove it, then re-run") from exc
|
|
115
|
+
if not isinstance(data, dict) or not isinstance(data.get("mcpServers", {}), dict):
|
|
116
|
+
raise AttachError(f"{path}: expected an object with an 'mcpServers' object")
|
|
117
|
+
servers = data.setdefault("mcpServers", {})
|
|
118
|
+
action = "unchanged" if servers.get(SERVER_NAME) == entry else ("updated" if SERVER_NAME in servers else "added")
|
|
119
|
+
servers[SERVER_NAME] = entry
|
|
120
|
+
return action, json.dumps(data, indent=2) + "\n"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
_OUR_TABLE = re.compile(r'^\s*\[\s*mcp_servers\s*\.\s*(?:memkernel|"memkernel")\s*(?:\.[^\]]*)?\]\s*(?:#.*)?$')
|
|
124
|
+
_ANY_TABLE = re.compile(r"^\s*\[")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _toml_str(value: str) -> str:
|
|
128
|
+
return json.dumps(value) # JSON string escapes are valid TOML basic strings
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def merge_toml_config(path: Path, entry: dict[str, Any]) -> tuple[str, str]:
|
|
132
|
+
"""Replace (or append) the ``[mcp_servers.memkernel]`` tables in a Codex
|
|
133
|
+
``config.toml``. The result is re-parsed and verified before returning."""
|
|
134
|
+
original = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
135
|
+
try:
|
|
136
|
+
existing = tomllib.loads(original).get("mcp_servers", {}).get(SERVER_NAME)
|
|
137
|
+
except tomllib.TOMLDecodeError as exc:
|
|
138
|
+
raise AttachError(f"{path} is not valid TOML ({exc}); fix it, then re-run") from exc
|
|
139
|
+
expected = dict(entry, default_tools_approval_mode="approve", startup_timeout_sec=CODEX_STARTUP_TIMEOUT_SEC)
|
|
140
|
+
if existing == expected:
|
|
141
|
+
return "unchanged", original
|
|
142
|
+
|
|
143
|
+
kept, skipping = [], False
|
|
144
|
+
for line in original.splitlines():
|
|
145
|
+
if _ANY_TABLE.match(line):
|
|
146
|
+
skipping = bool(_OUR_TABLE.match(line))
|
|
147
|
+
if not skipping:
|
|
148
|
+
kept.append(line)
|
|
149
|
+
block = [
|
|
150
|
+
f"[mcp_servers.{SERVER_NAME}]",
|
|
151
|
+
f"command = {_toml_str(entry['command'])}",
|
|
152
|
+
"args = [" + ", ".join(_toml_str(a) for a in entry["args"]) + "]",
|
|
153
|
+
"# Lets non-interactive `codex exec` call MemKernel tools (memory writes only).",
|
|
154
|
+
'default_tools_approval_mode = "approve"',
|
|
155
|
+
"# The MCP SDK import dominates cold start; allow for slow or heavily loaded machines.",
|
|
156
|
+
f"startup_timeout_sec = {CODEX_STARTUP_TIMEOUT_SEC}",
|
|
157
|
+
"",
|
|
158
|
+
f"[mcp_servers.{SERVER_NAME}.env]",
|
|
159
|
+
*(f"{k} = {_toml_str(v)}" for k, v in entry["env"].items()),
|
|
160
|
+
]
|
|
161
|
+
head = "\n".join(kept).rstrip()
|
|
162
|
+
text = (head + "\n\n" if head else "") + "\n".join(block) + "\n"
|
|
163
|
+
try:
|
|
164
|
+
parsed = tomllib.loads(text).get("mcp_servers", {}).get(SERVER_NAME)
|
|
165
|
+
except tomllib.TOMLDecodeError as exc:
|
|
166
|
+
parsed, detail = None, str(exc)
|
|
167
|
+
else:
|
|
168
|
+
detail = "entry did not round-trip"
|
|
169
|
+
if parsed != expected:
|
|
170
|
+
raise AttachError(
|
|
171
|
+
f"could not safely update {path} ({detail}); 'memkernel' may be defined with dotted keys. "
|
|
172
|
+
"Remove it from the file and re-run, or use `codex mcp add` (see docs/integrations.md)."
|
|
173
|
+
)
|
|
174
|
+
return ("updated" if existing is not None else "added"), text
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _ensure_gitignored(project: Path, dry_run: bool) -> str | None:
|
|
178
|
+
if not (project / ".git").exists():
|
|
179
|
+
return None
|
|
180
|
+
gitignore = project / ".gitignore"
|
|
181
|
+
lines = gitignore.read_text(encoding="utf-8").splitlines() if gitignore.exists() else []
|
|
182
|
+
if any(line.strip().rstrip("/") in {".memkernel", "/.memkernel"} for line in lines):
|
|
183
|
+
return None
|
|
184
|
+
if not dry_run:
|
|
185
|
+
prefix = "" if not lines or lines[-1] == "" else "\n"
|
|
186
|
+
with gitignore.open("a", encoding="utf-8") as fh:
|
|
187
|
+
fh.write(f"{prefix}# MemKernel shared agent memory (local database)\n.memkernel/\n")
|
|
188
|
+
return str(gitignore)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def attach(
|
|
192
|
+
clients: list[str],
|
|
193
|
+
*,
|
|
194
|
+
project: Path,
|
|
195
|
+
home: Path,
|
|
196
|
+
db: Path | None = None,
|
|
197
|
+
portable: bool = False,
|
|
198
|
+
agent_id: str | None = None,
|
|
199
|
+
dry_run: bool = False,
|
|
200
|
+
) -> list[dict[str, str]]:
|
|
201
|
+
"""Register the MemKernel server with each client. Returns a report."""
|
|
202
|
+
project = project.resolve()
|
|
203
|
+
report: list[dict[str, str]] = []
|
|
204
|
+
if not dry_run:
|
|
205
|
+
(project / ".memkernel").mkdir(parents=True, exist_ok=True) # anchors DB discovery
|
|
206
|
+
ignored = _ensure_gitignored(project, dry_run)
|
|
207
|
+
if ignored:
|
|
208
|
+
note = "added .memkernel/ to .gitignore" + (" (dry run)" if dry_run else "")
|
|
209
|
+
report.append({"client": "git", "path": ignored, "action": note})
|
|
210
|
+
for name in clients:
|
|
211
|
+
client = CLIENTS[name]
|
|
212
|
+
entry = build_entry(client, project=project, db=db, portable=portable, agent_id=agent_id)
|
|
213
|
+
for path in client.config_paths(project, home):
|
|
214
|
+
merge = merge_toml_config if client.fmt == "toml" else merge_json_config
|
|
215
|
+
action, text = merge(path, entry)
|
|
216
|
+
if action != "unchanged" and not dry_run:
|
|
217
|
+
if path.exists() and not client.project_scoped:
|
|
218
|
+
backup = path.with_name(path.name + ".memkernel.bak")
|
|
219
|
+
backup.write_text(path.read_text(encoding="utf-8"), encoding="utf-8")
|
|
220
|
+
_atomic_write(path, text)
|
|
221
|
+
report.append({"client": name, "path": str(path), "action": action + (" (dry run)" if dry_run else ""),
|
|
222
|
+
"hint": client.hint})
|
|
223
|
+
return report
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ------------------------------------------------------------------ doctor
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def _doctor(project: Path, home: Path) -> bool:
|
|
230
|
+
ok = True
|
|
231
|
+
|
|
232
|
+
def line(status: str, label: str, detail: str = "") -> None:
|
|
233
|
+
print(f" [{status}] {label}" + (f": {detail}" if detail else ""))
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
from mcp.server.fastmcp import FastMCP # noqa: F401
|
|
237
|
+
flavor = "FastMCP (mcp 1.x)"
|
|
238
|
+
except ModuleNotFoundError:
|
|
239
|
+
flavor = "MCPServer (mcp 2.x)"
|
|
240
|
+
print("MemKernel doctor")
|
|
241
|
+
line("ok", "python", sys.version.split()[0] + f" ({sys.executable})")
|
|
242
|
+
line("ok", "memkernel", _version())
|
|
243
|
+
line("ok", "mcp sdk", f"{metadata.version('mcp')} via {flavor}")
|
|
244
|
+
|
|
245
|
+
db = default_db_path(project)
|
|
246
|
+
try:
|
|
247
|
+
kernel = await MemKernel.open(db, auto_gc=False)
|
|
248
|
+
try:
|
|
249
|
+
branches = await kernel.store.list_branches()
|
|
250
|
+
problems = await kernel.verify(DEFAULT_BRANCH)
|
|
251
|
+
line("ok", "database", f"{db} (journal={kernel.store.journal_mode}, {len(branches)} branch(es))")
|
|
252
|
+
line("ok" if not problems else "FAIL", "hash chain (main)", "intact" if not problems else "; ".join(problems[:3]))
|
|
253
|
+
line("ok", "tokenizer", kernel.gc_engine.counter.backend_name)
|
|
254
|
+
ok &= not problems
|
|
255
|
+
finally:
|
|
256
|
+
await kernel.close()
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
line("FAIL", "database", f"{db}: {exc}")
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
from memkernel.clients.adapter import McpStdioClient
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
async with McpStdioClient(agent_id="memkernel-doctor", db_path=str(db), cwd=str(project)) as client:
|
|
265
|
+
tools = await asyncio.wait_for(client.list_tools(), 60)
|
|
266
|
+
state = await client.call("memkernel_query_state")
|
|
267
|
+
line("ok", "mcp server (stdio)", f"{len(tools)} tools; query_state head={state['head_event_id']}")
|
|
268
|
+
except Exception as exc:
|
|
269
|
+
line("FAIL", "mcp server (stdio)", repr(exc))
|
|
270
|
+
ok = False
|
|
271
|
+
|
|
272
|
+
print("Agent configurations:")
|
|
273
|
+
for name, client in CLIENTS.items():
|
|
274
|
+
for path in client.config_paths(project, home):
|
|
275
|
+
entry = _read_entry(path, client.fmt)
|
|
276
|
+
if entry is None:
|
|
277
|
+
line("--", name, f"not attached ({path}); run `memkernel attach {name}`")
|
|
278
|
+
continue
|
|
279
|
+
command = entry.get("command", "")
|
|
280
|
+
resolvable = Path(command).exists() or bool(_which(command))
|
|
281
|
+
line("ok" if resolvable else "FAIL", name, f"{path} -> {command}" + ("" if resolvable else " (command not found)"))
|
|
282
|
+
ok &= resolvable
|
|
283
|
+
return ok
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _which(command: str) -> str | None:
|
|
287
|
+
import shutil
|
|
288
|
+
|
|
289
|
+
return shutil.which(command)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _read_entry(path: Path, fmt: str) -> dict[str, Any] | None:
|
|
293
|
+
if not path.exists():
|
|
294
|
+
return None
|
|
295
|
+
try:
|
|
296
|
+
text = path.read_text(encoding="utf-8")
|
|
297
|
+
data = tomllib.loads(text).get("mcp_servers", {}) if fmt == "toml" else json.loads(text).get("mcpServers", {})
|
|
298
|
+
except Exception:
|
|
299
|
+
return None
|
|
300
|
+
entry = data.get(SERVER_NAME)
|
|
301
|
+
return entry if isinstance(entry, dict) else None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _version() -> str:
|
|
305
|
+
try:
|
|
306
|
+
return metadata.version("memkernel")
|
|
307
|
+
except metadata.PackageNotFoundError:
|
|
308
|
+
from memkernel import __version__
|
|
309
|
+
|
|
310
|
+
return __version__
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ------------------------------------------------------------------ status
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
async def _status(project: Path, branch: str, as_json: bool) -> None:
|
|
317
|
+
db = default_db_path(project)
|
|
318
|
+
if not db.exists():
|
|
319
|
+
print(f"No MemKernel database at {db}. Attach an agent (`memkernel attach ...`) and use it first.")
|
|
320
|
+
return
|
|
321
|
+
kernel = await MemKernel.open(db, auto_gc=False)
|
|
322
|
+
try:
|
|
323
|
+
state = await kernel.query_state(branch)
|
|
324
|
+
branches = await kernel.store.list_branches()
|
|
325
|
+
finally:
|
|
326
|
+
await kernel.close()
|
|
327
|
+
if as_json:
|
|
328
|
+
print(json.dumps({"db": str(db), "branches": [b.to_dict() for b in branches], "state": state}, indent=2))
|
|
329
|
+
return
|
|
330
|
+
s = state["state"]
|
|
331
|
+
print(f"database {db}")
|
|
332
|
+
print("branches " + ", ".join(f"{b.branch_id}(head #{b.head_event_id})" for b in branches))
|
|
333
|
+
print(f"branch {branch}: head #{state['head_event_id']}, context {state['context_tokens']} tokens, "
|
|
334
|
+
f"snapshot #{state['snapshot_event_id']}")
|
|
335
|
+
for title, items in (
|
|
336
|
+
("tasks", [f"{a}: {t['directive']}" for a, t in s["tasks"].items()]),
|
|
337
|
+
("decisions", [f"{k} = {d['value']} ({d['agent_id']})" for k, d in s["decisions"].items()]),
|
|
338
|
+
("facts", [f"{k} = {f['value']}" for k, f in s["active_workspace_facts"].items()]),
|
|
339
|
+
("blockers", [f"{k}: {b['description']}" for k, b in s["execution_blockers"].items()]),
|
|
340
|
+
("leases", [f"{l['resource_key']} held by {l['holder_agent_id']} ({l['expires_in_seconds']}s left)"
|
|
341
|
+
for l in state["active_leases"]]),
|
|
342
|
+
):
|
|
343
|
+
print(f"{title:<9} " + ("\n ".join(items) if items else "-"))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# -------------------------------------------------------------------- main
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def main(argv: list[str] | None = None) -> int:
|
|
350
|
+
parser = argparse.ArgumentParser(prog="memkernel", description="Shared, branchable memory for coding agents.")
|
|
351
|
+
parser.add_argument("--version", action="version", version=f"memkernel {_version()}")
|
|
352
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
353
|
+
|
|
354
|
+
p_attach = sub.add_parser("attach", help="register MemKernel with an agent's MCP configuration")
|
|
355
|
+
p_attach.add_argument("clients", nargs="+", choices=[*CLIENTS, "all"])
|
|
356
|
+
p_attach.add_argument("--project", type=Path, default=Path.cwd(), help="project root (default: current directory)")
|
|
357
|
+
p_attach.add_argument("--db", type=Path, help="pin a database path instead of per-project discovery")
|
|
358
|
+
p_attach.add_argument("--agent-id", help="override the agent identity written to the config")
|
|
359
|
+
p_attach.add_argument("--portable", action="store_true",
|
|
360
|
+
help="launch via `memkernel serve` from PATH with no absolute paths (for committed configs)")
|
|
361
|
+
p_attach.add_argument("--dry-run", action="store_true", help="show what would change without writing")
|
|
362
|
+
|
|
363
|
+
p_doctor = sub.add_parser("doctor", help="check installation, database, server and agent configs")
|
|
364
|
+
p_doctor.add_argument("--project", type=Path, default=Path.cwd())
|
|
365
|
+
|
|
366
|
+
p_status = sub.add_parser("status", help="show the memory agents share in this project")
|
|
367
|
+
p_status.add_argument("--project", type=Path, default=Path.cwd())
|
|
368
|
+
p_status.add_argument("--branch", default=DEFAULT_BRANCH)
|
|
369
|
+
p_status.add_argument("--json", action="store_true")
|
|
370
|
+
|
|
371
|
+
sub.add_parser("serve", help="run the MCP server over stdio (flags: see `memkernel serve --help`)", add_help=False)
|
|
372
|
+
|
|
373
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
374
|
+
if argv[:1] == ["serve"]:
|
|
375
|
+
from memkernel.server.mcp_server import main as serve
|
|
376
|
+
|
|
377
|
+
serve(argv[1:])
|
|
378
|
+
return 0
|
|
379
|
+
args = parser.parse_args(argv)
|
|
380
|
+
home = Path.home()
|
|
381
|
+
|
|
382
|
+
if args.command == "attach":
|
|
383
|
+
names = list(CLIENTS) if "all" in args.clients else list(dict.fromkeys(args.clients))
|
|
384
|
+
if args.agent_id and len(names) > 1:
|
|
385
|
+
parser.error("--agent-id applies to a single client")
|
|
386
|
+
try:
|
|
387
|
+
report = attach(names, project=args.project, home=home, db=args.db, portable=args.portable,
|
|
388
|
+
agent_id=args.agent_id, dry_run=args.dry_run)
|
|
389
|
+
except AttachError as exc:
|
|
390
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
391
|
+
return 1
|
|
392
|
+
for item in report:
|
|
393
|
+
print(f"{item['client']:<12} {item['action']:<10} {item['path']}")
|
|
394
|
+
print("\nNext steps:")
|
|
395
|
+
for hint in dict.fromkeys(item["hint"] for item in report if "hint" in item):
|
|
396
|
+
print(f" - {hint}")
|
|
397
|
+
print(" - Run `memkernel doctor` to verify everything end to end.")
|
|
398
|
+
return 0
|
|
399
|
+
if args.command == "doctor":
|
|
400
|
+
return 0 if asyncio.run(_doctor(args.project.resolve(), home)) else 1
|
|
401
|
+
if args.command == "status":
|
|
402
|
+
asyncio.run(_status(args.project.resolve(), args.branch, args.json))
|
|
403
|
+
return 0
|
|
404
|
+
parser.error(f"unknown command {args.command!r}")
|
|
405
|
+
return 2
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Client adapters for agent harnesses (in-process and MCP-over-stdio)."""
|