dbx-tools-graphiti 0.6.111__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.
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.3
2
+ Name: dbx-tools-graphiti
3
+ Version: 0.6.111
4
+ Summary: Native Graphiti MCP and Neo4j launcher with mise-managed prerequisites
5
+ Requires-Python: >=3.10
6
+ Project-URL: Source, https://github.com/reggie-db/dbx-tools/tree/main/packages/py/graphiti
7
+ Description-Content-Type: text/markdown
8
+
9
+ # `dbx-tools-graphiti`
10
+
11
+ Native launcher for [Graphiti](https://github.com/getzep/graphiti) with a local
12
+ Neo4j backend. It runs both services directly as host processes; it does not use
13
+ Docker, Podman, or another container runtime.
14
+
15
+ Install from PyPI:
16
+
17
+ ```bash
18
+ uv add dbx-tools-graphiti
19
+ ```
20
+
21
+ Or install the current `main` branch:
22
+
23
+ ```bash
24
+ uv add "dbx-tools-graphiti @ git+https://github.com/reggie-db/dbx-tools.git@main#subdirectory=packages/py/graphiti"
25
+ ```
26
+
27
+ ## Key features
28
+
29
+ - launches upstream Graphiti's HTTP MCP server at `http://127.0.0.1:8000/mcp/`;
30
+ - runs Neo4j Community 5.26 as a native background process;
31
+ - provisions Java 21 and `uv` through `mise use -g` only when absent;
32
+ - pins Graphiti and Neo4j versions for repeatable local environments;
33
+ - caches downloads, Python dependencies, Neo4j data, credentials, and logs;
34
+ - supports foreground or background operation without vendoring Graphiti code.
35
+
36
+ ## Quick start
37
+
38
+ `mise` must already be installed. The launcher handles Java and `uv` itself.
39
+
40
+ ```bash
41
+ export OPENAI_API_KEY=...
42
+ uv run dbx-graphiti start
43
+ ```
44
+
45
+ The first run downloads about 120 MB of Neo4j plus the pinned Graphiti release,
46
+ creates Graphiti's `uv` environment, generates a local Neo4j password, starts
47
+ Neo4j, and then runs Graphiti in the foreground. Later runs reuse all of it.
48
+
49
+ For background operation:
50
+
51
+ ```bash
52
+ uv run dbx-graphiti up
53
+ uv run dbx-graphiti status
54
+ uv run dbx-graphiti down
55
+ ```
56
+
57
+ ## Commands
58
+
59
+ | Command | Behavior |
60
+ | -------- | ----------------------------------------------------------------------------- |
61
+ | `setup` | Provision tools and populate the local cache without starting services. |
62
+ | `start` | Start Neo4j, then run Graphiti in the foreground. This is the default. |
63
+ | `up` | Start both services in the background. |
64
+ | `down` | Stop the managed Graphiti and Neo4j processes. |
65
+ | `status` | Print process state and the MCP URL as JSON. |
66
+ | `env` | Print resolved Neo4j connection settings as JSON. Treat its output as secret. |
67
+
68
+ Arguments after `start` or `up` are forwarded to upstream Graphiti. For example:
69
+
70
+ ```bash
71
+ uv run dbx-graphiti start -- --port 9000 --group-id my-agent
72
+ ```
73
+
74
+ ## Provisioning and caching
75
+
76
+ The package deliberately keeps orchestration separate from Graphiti itself:
77
+
78
+ 1. It checks `mise where java@21` and `mise where uv@0.11`.
79
+ 2. A missing tool is installed globally with `mise use -g --yes`.
80
+ 3. Neo4j Community `5.26.12` is downloaded from `dist.neo4j.org` and unpacked.
81
+ 4. Graphiti `v0.29.3` is downloaded from its GitHub release tag.
82
+ 5. `uv sync --project <checkout>/mcp_server` creates the upstream environment.
83
+ 6. A generated Neo4j password is stored with mode `0600` and supplied to
84
+ Graphiti through its documented environment variables.
85
+
86
+ The cache root is:
87
+
88
+ - macOS: `~/Library/Application Support/dbx-tools/graphiti`
89
+ - Linux: `${XDG_DATA_HOME:-~/.local/share}/dbx-tools/graphiti`
90
+ - Windows: `%LOCALAPPDATA%/dbx-tools/graphiti`
91
+
92
+ Set `DBX_GRAPHITI_HOME` to override it. Removing the directory clears the
93
+ download cache and permanently removes the local graph data.
94
+
95
+ ## Configuration
96
+
97
+ The packaged default fixes the database provider to Neo4j and otherwise follows
98
+ upstream Graphiti environment names. Common settings are:
99
+
100
+ | Variable | Default |
101
+ | ------------------- | ------------------------ |
102
+ | `OPENAI_API_KEY` | Required |
103
+ | `MODEL_NAME` | `gpt-4.1-mini` |
104
+ | `EMBEDDER_MODEL` | `text-embedding-3-small` |
105
+ | `GRAPHITI_GROUP_ID` | `main` |
106
+ | `GRAPHITI_HOST` | `127.0.0.1` |
107
+ | `GRAPHITI_PORT` | `8000` |
108
+ | `NEO4J_URI` | `bolt://127.0.0.1:7687` |
109
+ | `NEO4J_DATABASE` | `neo4j` |
110
+
111
+ Explicit `NEO4J_*` values override generated defaults, which lets the Graphiti
112
+ process use an existing Neo4j server. The launcher still manages its local
113
+ Neo4j process; use upstream Graphiti directly if lifecycle ownership belongs to
114
+ an external database administrator.
115
+
116
+ Graphiti owns MCP tools, graph behavior, LLM calls, embeddings, and migrations.
117
+ This package owns only repeatable installation, configuration, and process
118
+ lifecycle. See the [upstream MCP server documentation](https://github.com/getzep/graphiti/tree/main/mcp_server)
119
+ for its complete API and provider configuration.
@@ -0,0 +1,111 @@
1
+ # `dbx-tools-graphiti`
2
+
3
+ Native launcher for [Graphiti](https://github.com/getzep/graphiti) with a local
4
+ Neo4j backend. It runs both services directly as host processes; it does not use
5
+ Docker, Podman, or another container runtime.
6
+
7
+ Install from PyPI:
8
+
9
+ ```bash
10
+ uv add dbx-tools-graphiti
11
+ ```
12
+
13
+ Or install the current `main` branch:
14
+
15
+ ```bash
16
+ uv add "dbx-tools-graphiti @ git+https://github.com/reggie-db/dbx-tools.git@main#subdirectory=packages/py/graphiti"
17
+ ```
18
+
19
+ ## Key features
20
+
21
+ - launches upstream Graphiti's HTTP MCP server at `http://127.0.0.1:8000/mcp/`;
22
+ - runs Neo4j Community 5.26 as a native background process;
23
+ - provisions Java 21 and `uv` through `mise use -g` only when absent;
24
+ - pins Graphiti and Neo4j versions for repeatable local environments;
25
+ - caches downloads, Python dependencies, Neo4j data, credentials, and logs;
26
+ - supports foreground or background operation without vendoring Graphiti code.
27
+
28
+ ## Quick start
29
+
30
+ `mise` must already be installed. The launcher handles Java and `uv` itself.
31
+
32
+ ```bash
33
+ export OPENAI_API_KEY=...
34
+ uv run dbx-graphiti start
35
+ ```
36
+
37
+ The first run downloads about 120 MB of Neo4j plus the pinned Graphiti release,
38
+ creates Graphiti's `uv` environment, generates a local Neo4j password, starts
39
+ Neo4j, and then runs Graphiti in the foreground. Later runs reuse all of it.
40
+
41
+ For background operation:
42
+
43
+ ```bash
44
+ uv run dbx-graphiti up
45
+ uv run dbx-graphiti status
46
+ uv run dbx-graphiti down
47
+ ```
48
+
49
+ ## Commands
50
+
51
+ | Command | Behavior |
52
+ | -------- | ----------------------------------------------------------------------------- |
53
+ | `setup` | Provision tools and populate the local cache without starting services. |
54
+ | `start` | Start Neo4j, then run Graphiti in the foreground. This is the default. |
55
+ | `up` | Start both services in the background. |
56
+ | `down` | Stop the managed Graphiti and Neo4j processes. |
57
+ | `status` | Print process state and the MCP URL as JSON. |
58
+ | `env` | Print resolved Neo4j connection settings as JSON. Treat its output as secret. |
59
+
60
+ Arguments after `start` or `up` are forwarded to upstream Graphiti. For example:
61
+
62
+ ```bash
63
+ uv run dbx-graphiti start -- --port 9000 --group-id my-agent
64
+ ```
65
+
66
+ ## Provisioning and caching
67
+
68
+ The package deliberately keeps orchestration separate from Graphiti itself:
69
+
70
+ 1. It checks `mise where java@21` and `mise where uv@0.11`.
71
+ 2. A missing tool is installed globally with `mise use -g --yes`.
72
+ 3. Neo4j Community `5.26.12` is downloaded from `dist.neo4j.org` and unpacked.
73
+ 4. Graphiti `v0.29.3` is downloaded from its GitHub release tag.
74
+ 5. `uv sync --project <checkout>/mcp_server` creates the upstream environment.
75
+ 6. A generated Neo4j password is stored with mode `0600` and supplied to
76
+ Graphiti through its documented environment variables.
77
+
78
+ The cache root is:
79
+
80
+ - macOS: `~/Library/Application Support/dbx-tools/graphiti`
81
+ - Linux: `${XDG_DATA_HOME:-~/.local/share}/dbx-tools/graphiti`
82
+ - Windows: `%LOCALAPPDATA%/dbx-tools/graphiti`
83
+
84
+ Set `DBX_GRAPHITI_HOME` to override it. Removing the directory clears the
85
+ download cache and permanently removes the local graph data.
86
+
87
+ ## Configuration
88
+
89
+ The packaged default fixes the database provider to Neo4j and otherwise follows
90
+ upstream Graphiti environment names. Common settings are:
91
+
92
+ | Variable | Default |
93
+ | ------------------- | ------------------------ |
94
+ | `OPENAI_API_KEY` | Required |
95
+ | `MODEL_NAME` | `gpt-4.1-mini` |
96
+ | `EMBEDDER_MODEL` | `text-embedding-3-small` |
97
+ | `GRAPHITI_GROUP_ID` | `main` |
98
+ | `GRAPHITI_HOST` | `127.0.0.1` |
99
+ | `GRAPHITI_PORT` | `8000` |
100
+ | `NEO4J_URI` | `bolt://127.0.0.1:7687` |
101
+ | `NEO4J_DATABASE` | `neo4j` |
102
+
103
+ Explicit `NEO4J_*` values override generated defaults, which lets the Graphiti
104
+ process use an existing Neo4j server. The launcher still manages its local
105
+ Neo4j process; use upstream Graphiti directly if lifecycle ownership belongs to
106
+ an external database administrator.
107
+
108
+ Graphiti owns MCP tools, graph behavior, LLM calls, embeddings, and migrations.
109
+ This package owns only repeatable installation, configuration, and process
110
+ lifecycle. See the [upstream MCP server documentation](https://github.com/getzep/graphiti/tree/main/mcp_server)
111
+ for its complete API and provider configuration.
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "dbx-tools-graphiti"
3
+ version = "0.6.111"
4
+ description = "Native Graphiti MCP and Neo4j launcher with mise-managed prerequisites"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = []
8
+
9
+ [project.urls]
10
+ Source = "https://github.com/reggie-db/dbx-tools/tree/main/packages/py/graphiti"
11
+
12
+ [project.scripts]
13
+ dbx-graphiti = "dbx_tools.graphiti.cli:main"
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.11.28,<0.12.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [tool.uv.build-backend]
20
+ module-name = "dbx_tools.graphiti"
21
+ module-root = "src"
22
+ namespace = true
@@ -0,0 +1,24 @@
1
+ # ~~ Generated by projen. To modify, edit .projenrc.js and run "bunx projen".
2
+
3
+ [project]
4
+ name = "dbx-tools-graphiti"
5
+ version = "0.6.111"
6
+ description = "Native Graphiti MCP and Neo4j launcher with mise-managed prerequisites"
7
+ readme = "README.md"
8
+ requires-python = ">=3.10"
9
+ dependencies = [ ]
10
+
11
+ [project.urls]
12
+ Source = "https://github.com/reggie-db/dbx-tools/tree/main/packages/py/graphiti"
13
+
14
+ [project.scripts]
15
+ dbx-graphiti = "dbx_tools.graphiti.cli:main"
16
+
17
+ [build-system]
18
+ requires = [ "uv_build>=0.11.28,<0.12.0" ]
19
+ build-backend = "uv_build"
20
+
21
+ [tool.uv.build-backend]
22
+ module-name = "dbx_tools.graphiti"
23
+ module-root = "src"
24
+ namespace = true
@@ -0,0 +1,5 @@
1
+ """Native Graphiti MCP and Neo4j launcher."""
2
+
3
+ from .runtime import GRAPHITI_VERSION, NEO4J_VERSION, RuntimePaths
4
+
5
+ __all__ = ["GRAPHITI_VERSION", "NEO4J_VERSION", "RuntimePaths"]
@@ -0,0 +1,5 @@
1
+ """Run the Graphiti launcher."""
2
+
3
+ from .cli import main
4
+
5
+ main()
@@ -0,0 +1,51 @@
1
+ """Command-line interface for the native Graphiti stack."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ from collections.abc import Sequence
9
+
10
+ from .runtime import Runtime
11
+
12
+
13
+ def main(argv: Sequence[str] | None = None) -> None:
14
+ parser = argparse.ArgumentParser(
15
+ prog="dbx-graphiti",
16
+ description="Run Graphiti MCP with a local native Neo4j backend (no containers).",
17
+ )
18
+ subparsers = parser.add_subparsers(dest="command")
19
+ subparsers.add_parser("setup", help="Install pinned native prerequisites")
20
+ start = subparsers.add_parser("start", help="Start Neo4j and Graphiti in the foreground")
21
+ start.add_argument("graphiti_args", nargs=argparse.REMAINDER)
22
+ up = subparsers.add_parser("up", help="Start Neo4j and Graphiti in the background")
23
+ up.add_argument("graphiti_args", nargs=argparse.REMAINDER)
24
+ subparsers.add_parser("down", help="Stop Graphiti and Neo4j")
25
+ subparsers.add_parser("status", help="Show native process status")
26
+ subparsers.add_parser("env", help="Print resolved connection settings")
27
+
28
+ parsed = parser.parse_args(argv)
29
+ runtime = Runtime()
30
+ command = parsed.command or "start"
31
+ if command == "setup":
32
+ runtime.setup()
33
+ print(f"Graphiti is ready under {runtime.paths.root}")
34
+ elif command in {"start", "up"}:
35
+ if not os.getenv("OPENAI_API_KEY"):
36
+ parser.error("OPENAI_API_KEY is required by the default Graphiti configuration")
37
+ extra_args = getattr(parsed, "graphiti_args", [])
38
+ if extra_args[:1] == ["--"]:
39
+ extra_args = extra_args[1:]
40
+ result = runtime.start(foreground=command == "start", extra_args=extra_args)
41
+ if command == "up":
42
+ print(f"Graphiti started with PID {result}; MCP: http://127.0.0.1:8000/mcp/")
43
+ elif result:
44
+ raise SystemExit(result)
45
+ elif command == "down":
46
+ runtime.stop()
47
+ elif command == "status":
48
+ print(json.dumps(runtime.status(), indent=2))
49
+ elif command == "env":
50
+ state = runtime.read_state()
51
+ print(json.dumps(runtime.connection_settings(str(state["neo4j_password"])), indent=2))
@@ -0,0 +1,41 @@
1
+ server:
2
+ transport: "http"
3
+ host: ${GRAPHITI_HOST:127.0.0.1}
4
+ port: ${GRAPHITI_PORT:8000}
5
+
6
+ llm:
7
+ provider: "openai"
8
+ model: ${MODEL_NAME:gpt-4.1-mini}
9
+ max_tokens: 4096
10
+ structured_output_mode: ${LLM_STRUCTURED_OUTPUT_MODE:json_schema}
11
+ providers:
12
+ openai:
13
+ api_key: ${OPENAI_API_KEY}
14
+ api_url: ${OPENAI_API_URL:https://api.openai.com/v1}
15
+ organization_id: ${OPENAI_ORGANIZATION_ID:}
16
+
17
+ embedder:
18
+ provider: "openai"
19
+ model: ${EMBEDDER_MODEL:text-embedding-3-small}
20
+ dimensions: 1536
21
+ providers:
22
+ openai:
23
+ api_key: ${OPENAI_API_KEY}
24
+ api_url: ${OPENAI_API_URL:https://api.openai.com/v1}
25
+ organization_id: ${OPENAI_ORGANIZATION_ID:}
26
+
27
+ database:
28
+ provider: "neo4j"
29
+ providers:
30
+ neo4j:
31
+ uri: ${NEO4J_URI:bolt://127.0.0.1:7687}
32
+ username: ${NEO4J_USER:neo4j}
33
+ password: ${NEO4J_PASSWORD}
34
+ database: ${NEO4J_DATABASE:neo4j}
35
+ use_parallel_runtime: false
36
+
37
+ graphiti:
38
+ group_id: ${GRAPHITI_GROUP_ID:main}
39
+ episode_id_prefix: ${EPISODE_ID_PREFIX:}
40
+ user_id: ${USER_ID:mcp_user}
41
+ entity_types: []
@@ -0,0 +1,318 @@
1
+ """Installation and native process lifecycle for Graphiti and Neo4j."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import secrets
8
+ import shutil
9
+ import signal
10
+ import subprocess
11
+ import sys
12
+ import tarfile
13
+ import time
14
+ import urllib.request
15
+ from dataclasses import dataclass
16
+ from importlib import resources
17
+ from pathlib import Path
18
+
19
+ GRAPHITI_VERSION = "0.29.3"
20
+ NEO4J_VERSION = "5.26.12"
21
+ JAVA_VERSION = "21"
22
+ UV_VERSION = "0.11"
23
+
24
+
25
+ def default_data_dir() -> Path:
26
+ """Return the per-user directory containing downloads, data, and logs."""
27
+ override = os.getenv("DBX_GRAPHITI_HOME")
28
+ if override:
29
+ return Path(override).expanduser()
30
+ if sys.platform == "darwin":
31
+ return Path.home() / "Library" / "Application Support" / "dbx-tools" / "graphiti"
32
+ if os.name == "nt":
33
+ return (
34
+ Path(os.getenv("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
35
+ / "dbx-tools"
36
+ / "graphiti"
37
+ )
38
+ return (
39
+ Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share"))
40
+ / "dbx-tools"
41
+ / "graphiti"
42
+ )
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class RuntimePaths:
47
+ """Resolved filesystem layout for one Graphiti installation."""
48
+
49
+ root: Path
50
+
51
+ @classmethod
52
+ def default(cls) -> RuntimePaths:
53
+ return cls(default_data_dir())
54
+
55
+ @property
56
+ def graphiti(self) -> Path:
57
+ return self.root / "graphiti" / GRAPHITI_VERSION
58
+
59
+ @property
60
+ def neo4j(self) -> Path:
61
+ return self.root / "neo4j" / NEO4J_VERSION
62
+
63
+ @property
64
+ def neo4j_data(self) -> Path:
65
+ return self.root / "data" / "neo4j"
66
+
67
+ @property
68
+ def state(self) -> Path:
69
+ return self.root / "state.json"
70
+
71
+ @property
72
+ def log(self) -> Path:
73
+ return self.root / "graphiti.log"
74
+
75
+
76
+ class Runtime:
77
+ """Provision and run a pinned native Graphiti stack."""
78
+
79
+ def __init__(self, paths: RuntimePaths | None = None) -> None:
80
+ self.paths = paths or RuntimePaths.default()
81
+
82
+ def setup(self) -> None:
83
+ self._require_mise()
84
+ self._ensure_mise_tool("java", JAVA_VERSION)
85
+ self._ensure_mise_tool("uv", UV_VERSION)
86
+ self.paths.root.mkdir(parents=True, exist_ok=True)
87
+ self._install_neo4j()
88
+ self._install_graphiti()
89
+ self._ensure_state()
90
+
91
+ def start(self, *, foreground: bool = True, extra_args: list[str] | None = None) -> int:
92
+ self.setup()
93
+ state = self.read_state()
94
+ self._start_neo4j(state["neo4j_password"])
95
+ command = self.graphiti_command(extra_args or [])
96
+ environment = self.environment(state["neo4j_password"])
97
+ if foreground:
98
+ return subprocess.call(command, cwd=self.paths.graphiti / "mcp_server", env=environment)
99
+ with self.paths.log.open("ab") as output:
100
+ process = subprocess.Popen(
101
+ command,
102
+ cwd=self.paths.graphiti / "mcp_server",
103
+ env=environment,
104
+ stdout=output,
105
+ stderr=subprocess.STDOUT,
106
+ start_new_session=True,
107
+ )
108
+ state["graphiti_pid"] = process.pid
109
+ self._write_state(state)
110
+ return process.pid
111
+
112
+ def stop(self) -> None:
113
+ state = self.read_state(required=False)
114
+ pid = state.pop("graphiti_pid", None)
115
+ if pid and _is_running(pid):
116
+ os.kill(pid, signal.SIGTERM)
117
+ if (self.paths.neo4j / "bin" / "neo4j").exists():
118
+ self._neo4j_command("stop", check=False)
119
+ if state:
120
+ self._write_state(state)
121
+
122
+ def status(self) -> dict[str, object]:
123
+ state = self.read_state(required=False)
124
+ pid = state.get("graphiti_pid")
125
+ neo4j_running = False
126
+ if (self.paths.neo4j / "bin" / "neo4j").exists():
127
+ result = self._neo4j_command("status", check=False, capture_output=True)
128
+ neo4j_running = result.returncode == 0
129
+ return {
130
+ "home": str(self.paths.root),
131
+ "graphiti": "running" if isinstance(pid, int) and _is_running(pid) else "stopped",
132
+ "graphiti_pid": pid,
133
+ "neo4j": "running" if neo4j_running else "stopped",
134
+ "mcp_url": "http://127.0.0.1:8000/mcp/",
135
+ }
136
+
137
+ def graphiti_command(self, extra_args: list[str]) -> list[str]:
138
+ config = resources.files("dbx_tools.graphiti").joinpath("config.yaml")
139
+ return [
140
+ "mise",
141
+ "exec",
142
+ f"uv@{UV_VERSION}",
143
+ "--",
144
+ "uv",
145
+ "run",
146
+ "--project",
147
+ str(self.paths.graphiti / "mcp_server"),
148
+ "python",
149
+ str(self.paths.graphiti / "mcp_server" / "main.py"),
150
+ "--config",
151
+ str(config),
152
+ "--database-provider",
153
+ "neo4j",
154
+ *extra_args,
155
+ ]
156
+
157
+ def environment(self, password: str) -> dict[str, str]:
158
+ environment = os.environ.copy()
159
+ environment.setdefault("NEO4J_URI", "bolt://127.0.0.1:7687")
160
+ environment.setdefault("NEO4J_USER", "neo4j")
161
+ environment.setdefault("NEO4J_PASSWORD", password)
162
+ environment.setdefault("NEO4J_DATABASE", "neo4j")
163
+ return environment
164
+
165
+ def connection_settings(self, password: str) -> dict[str, str]:
166
+ """Return only the Neo4j settings callers need to connect."""
167
+ environment = self.environment(password)
168
+ return {
169
+ name: environment[name]
170
+ for name in ("NEO4J_URI", "NEO4J_USER", "NEO4J_PASSWORD", "NEO4J_DATABASE")
171
+ }
172
+
173
+ def _require_mise(self) -> None:
174
+ if not shutil.which("mise"):
175
+ raise RuntimeError("mise is required; install it from https://mise.jdx.dev")
176
+
177
+ def _ensure_mise_tool(self, tool: str, version: str) -> None:
178
+ result = subprocess.run(
179
+ ["mise", "where", f"{tool}@{version}"],
180
+ stdout=subprocess.DEVNULL,
181
+ stderr=subprocess.DEVNULL,
182
+ check=False,
183
+ )
184
+ if result.returncode:
185
+ subprocess.run(["mise", "use", "-g", "--yes", f"{tool}@{version}"], check=True)
186
+
187
+ def _install_neo4j(self) -> None:
188
+ if (self.paths.neo4j / "bin" / "neo4j").exists():
189
+ return
190
+ archive = self.paths.root / f"neo4j-community-{NEO4J_VERSION}-unix.tar.gz"
191
+ url = f"https://dist.neo4j.org/neo4j-community-{NEO4J_VERSION}-unix.tar.gz"
192
+ _download(url, archive)
193
+ target_parent = self.paths.neo4j.parent
194
+ target_parent.mkdir(parents=True, exist_ok=True)
195
+ with tarfile.open(archive, "r:gz") as bundle:
196
+ _extract_archive(bundle, target_parent)
197
+ extracted = target_parent / f"neo4j-community-{NEO4J_VERSION}"
198
+ extracted.rename(self.paths.neo4j)
199
+ archive.unlink()
200
+
201
+ def _install_graphiti(self) -> None:
202
+ if (self.paths.graphiti / "mcp_server" / "main.py").exists():
203
+ return
204
+ archive = self.paths.root / f"graphiti-{GRAPHITI_VERSION}.tar.gz"
205
+ url = f"https://github.com/getzep/graphiti/archive/refs/tags/v{GRAPHITI_VERSION}.tar.gz"
206
+ _download(url, archive)
207
+ target_parent = self.paths.graphiti.parent
208
+ target_parent.mkdir(parents=True, exist_ok=True)
209
+ with tarfile.open(archive, "r:gz") as bundle:
210
+ _extract_archive(bundle, target_parent)
211
+ extracted = target_parent / f"graphiti-{GRAPHITI_VERSION}"
212
+ extracted.rename(self.paths.graphiti)
213
+ archive.unlink()
214
+ subprocess.run(
215
+ [
216
+ "mise",
217
+ "exec",
218
+ f"uv@{UV_VERSION}",
219
+ "--",
220
+ "uv",
221
+ "sync",
222
+ "--project",
223
+ str(self.paths.graphiti / "mcp_server"),
224
+ ],
225
+ check=True,
226
+ )
227
+
228
+ def _ensure_state(self) -> None:
229
+ if self.paths.state.exists():
230
+ return
231
+ password = secrets.token_urlsafe(24)
232
+ self._write_state({"neo4j_password": password})
233
+ subprocess.run(
234
+ self._mise_java_command(
235
+ self.paths.neo4j / "bin" / "neo4j-admin",
236
+ "dbms",
237
+ "set-initial-password",
238
+ password,
239
+ ),
240
+ env=self._neo4j_environment(),
241
+ check=True,
242
+ )
243
+
244
+ def _start_neo4j(self, password: str) -> None:
245
+ del password
246
+ result = self._neo4j_command("status", check=False)
247
+ if result.returncode:
248
+ self._neo4j_command("start")
249
+ deadline = time.monotonic() + 60
250
+ while time.monotonic() < deadline:
251
+ result = self._neo4j_command("status", check=False)
252
+ if result.returncode == 0:
253
+ return
254
+ time.sleep(1)
255
+ raise RuntimeError("Neo4j did not become ready within 60 seconds")
256
+
257
+ def _neo4j_command(
258
+ self,
259
+ action: str,
260
+ *,
261
+ check: bool = False,
262
+ capture_output: bool = False,
263
+ ) -> subprocess.CompletedProcess[str]:
264
+ return subprocess.run(
265
+ self._mise_java_command(self.paths.neo4j / "bin" / "neo4j", action),
266
+ env=self._neo4j_environment(),
267
+ text=True,
268
+ check=check,
269
+ capture_output=capture_output,
270
+ )
271
+
272
+ def _mise_java_command(self, executable: Path, *arguments: str) -> list[str]:
273
+ return ["mise", "exec", f"java@{JAVA_VERSION}", "--", str(executable), *arguments]
274
+
275
+ def _neo4j_environment(self) -> dict[str, str]:
276
+ environment = os.environ.copy()
277
+ environment["NEO4J_HOME"] = str(self.paths.neo4j)
278
+ environment["NEO4J_CONF"] = str(self.paths.neo4j / "conf")
279
+ environment["NEO4J_server_directories_data"] = str(self.paths.neo4j_data)
280
+ environment["NEO4J_server_default__listen__address"] = "127.0.0.1"
281
+ return environment
282
+
283
+ def read_state(self, *, required: bool = True) -> dict[str, object]:
284
+ if not self.paths.state.exists():
285
+ if required:
286
+ raise RuntimeError("Graphiti is not set up; run `dbx-graphiti setup`")
287
+ return {}
288
+ return json.loads(self.paths.state.read_text())
289
+
290
+ def _write_state(self, state: dict[str, object]) -> None:
291
+ self.paths.root.mkdir(parents=True, exist_ok=True)
292
+ self.paths.state.write_text(json.dumps(state, indent=2) + "\n")
293
+ self.paths.state.chmod(0o600)
294
+
295
+
296
+ def _download(url: str, target: Path) -> None:
297
+ target.parent.mkdir(parents=True, exist_ok=True)
298
+ temporary = target.with_suffix(f"{target.suffix}.part")
299
+ with urllib.request.urlopen(url) as response, temporary.open("wb") as output:
300
+ shutil.copyfileobj(response, output)
301
+ temporary.replace(target)
302
+
303
+
304
+ def _extract_archive(bundle: tarfile.TarFile, destination: Path) -> None:
305
+ destination = destination.resolve()
306
+ for member in bundle.getmembers():
307
+ extracted = (destination / member.name).resolve()
308
+ if destination not in extracted.parents and extracted != destination:
309
+ raise RuntimeError(f"Archive member escapes destination: {member.name}")
310
+ bundle.extractall(destination)
311
+
312
+
313
+ def _is_running(pid: int) -> bool:
314
+ try:
315
+ os.kill(pid, 0)
316
+ except OSError:
317
+ return False
318
+ return True