memgraph-context-graph 0.7.2__tar.gz → 0.7.4__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.
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/PKG-INFO +1 -1
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/pyproject.toml +1 -1
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/codex_memory.py +52 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/cli.py +19 -3
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/agents.py +35 -3
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/cli.py +8 -3
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/engine.py +60 -6
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/prompts.py +16 -13
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/tool.py +27 -8
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/spaces.py +4 -2
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_dev_scripts.py +1 -1
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_setup.py +166 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/.gitignore +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/CONTEXT.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/LICENSE +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/README.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/adr/0003-config-path-override.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/command-hooks.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/research/2026-10-codex-memory.md +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/__init__.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/__init__.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/_identity.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/_spec.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/antigravity_cli.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/claude.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/claude_code.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/claude_code_memory.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/codex.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/copilot_cli.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/cursor.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/grok.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/openai.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/opencode.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/capture_log.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/events.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/hooks/__init__.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/hooks/cli.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/hooks/runner.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/hooks/runtime_plugin.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/link.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/mcp_server.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/memory_backend.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/protocols.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/__init__.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/memgraph.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/uninstall.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/tools.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/__init__.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/conftest.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/fake_agent_cli.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_antigravity_cli_adapter.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_capture_log.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_claude_code_adapter.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_codex_adapter.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_config_cli.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_context_before_tool.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_copilot_cli_adapter.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_cursor_adapter.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_events.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_grok_adapter.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_hook_cli.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_identity.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_link.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_opencode_adapter.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_plugin_hooks.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_runner_sessions_graph_connector.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_runtime_plugin.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_setup_docker.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_spaces.py +0 -0
- {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_tools.py +0 -0
|
@@ -126,6 +126,58 @@ class CodexMemory:
|
|
|
126
126
|
return report
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
def allow_plugin_tools(home: Path, plugin_id: str) -> list[str]:
|
|
130
|
+
"""Let the plugin's MCP tools run without a prompt, unless the user already set a mode for them.
|
|
131
|
+
|
|
132
|
+
Codex asks before every MCP tool call, and under ``approval_policy = "never"`` an ask is a refusal, so
|
|
133
|
+
``recall`` would fail for exactly the users who turned prompts off. The tools are read-only over the user's
|
|
134
|
+
own memory (``memory`` writes only ``/memories`` files), which is what ``approve`` is for.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
MemoryBackendError: the config file isn't valid TOML.
|
|
138
|
+
"""
|
|
139
|
+
path = _config_path(home)
|
|
140
|
+
config = _read(path)
|
|
141
|
+
server = _plugin_server(config, plugin_id, create=True)
|
|
142
|
+
if "default_tools_approval_mode" in server:
|
|
143
|
+
return [f"Unchanged {path}"]
|
|
144
|
+
server["default_tools_approval_mode"] = "approve"
|
|
145
|
+
return _write(path, config)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def disallow_plugin_tools(home: Path, plugin_id: str) -> list[str]:
|
|
149
|
+
"""Drop what :func:`allow_plugin_tools` wrote, leaving the rest of the plugin's table as the user has it."""
|
|
150
|
+
path = _config_path(home)
|
|
151
|
+
if not path.exists():
|
|
152
|
+
return []
|
|
153
|
+
config = _read(path)
|
|
154
|
+
server = _plugin_server(config, plugin_id, create=False)
|
|
155
|
+
if server is None or server.get("default_tools_approval_mode") != "approve":
|
|
156
|
+
return []
|
|
157
|
+
del server["default_tools_approval_mode"]
|
|
158
|
+
plugin = config["plugins"][plugin_id]
|
|
159
|
+
if not server:
|
|
160
|
+
del plugin["mcp_servers"][_SERVER]
|
|
161
|
+
if not plugin["mcp_servers"]:
|
|
162
|
+
del plugin["mcp_servers"]
|
|
163
|
+
if not plugin:
|
|
164
|
+
del config["plugins"][plugin_id]
|
|
165
|
+
return _write(path, config)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _plugin_server(config: Any, plugin_id: str, *, create: bool) -> Any:
|
|
169
|
+
import tomlkit
|
|
170
|
+
|
|
171
|
+
node = config
|
|
172
|
+
for key in ("plugins", plugin_id, "mcp_servers", _SERVER):
|
|
173
|
+
if key not in node:
|
|
174
|
+
if not create:
|
|
175
|
+
return None
|
|
176
|
+
node[key] = tomlkit.table(is_super_table=key != _SERVER)
|
|
177
|
+
node = node[key]
|
|
178
|
+
return node
|
|
179
|
+
|
|
180
|
+
|
|
129
181
|
def _codex_home(home: Path) -> Path:
|
|
130
182
|
override = os.environ.get("CODEX_HOME")
|
|
131
183
|
return Path(override) if override else home / ".codex"
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/cli.py
RENAMED
|
@@ -257,9 +257,9 @@ def _ui(argv: list[str]) -> int:
|
|
|
257
257
|
from context_graph_viewer.cli import main as viewer_main
|
|
258
258
|
except ImportError:
|
|
259
259
|
print(
|
|
260
|
-
"memgraph-context-graph ui needs the context-graph-viewer package
|
|
261
|
-
"
|
|
262
|
-
"
|
|
260
|
+
"memgraph-context-graph ui needs the context-graph-viewer package, which isn't installed.\n"
|
|
261
|
+
"Run setup again and say yes to the viewer:\n"
|
|
262
|
+
" memgraph-context-graph setup",
|
|
263
263
|
file=sys.stderr,
|
|
264
264
|
)
|
|
265
265
|
return 1
|
|
@@ -305,6 +305,7 @@ def _join(argv: list[str]) -> int:
|
|
|
305
305
|
parser.add_argument("--password", default=None, help="Prompted for when omitted.")
|
|
306
306
|
args = parser.parse_args(argv)
|
|
307
307
|
password = args.password if args.password is not None else getpass.getpass(f"Password for {args.login}: ")
|
|
308
|
+
_ensure_enterprise_package()
|
|
308
309
|
|
|
309
310
|
write_config(
|
|
310
311
|
edition=EDITION_ENTERPRISE, memgraph_url=args.url, memgraph_user=args.login, memgraph_password=password
|
|
@@ -328,6 +329,21 @@ def _join(argv: list[str]) -> int:
|
|
|
328
329
|
return 0
|
|
329
330
|
|
|
330
331
|
|
|
332
|
+
def _ensure_enterprise_package() -> None:
|
|
333
|
+
"""Install ``context-graph-spaces`` when this is the uv tool and it is missing; else the error says how."""
|
|
334
|
+
from memgraph_context_graph.setup import tool
|
|
335
|
+
from memgraph_context_graph.spaces import EDITION_ENTERPRISE, _edition_factory
|
|
336
|
+
|
|
337
|
+
if _edition_factory(EDITION_ENTERPRISE) is not None or not tool.uv_tool_environment():
|
|
338
|
+
return
|
|
339
|
+
print("Enterprise spaces need one more package, with the local extraction models; installing it.", flush=True)
|
|
340
|
+
print("This can take a minute.", flush=True)
|
|
341
|
+
try:
|
|
342
|
+
tool.install(viewer=False, spaces=True, log=lambda line: None)
|
|
343
|
+
except tool.ToolError as exc:
|
|
344
|
+
print(str(exc), file=sys.stderr)
|
|
345
|
+
|
|
346
|
+
|
|
331
347
|
def _doctor(argv: list[str]) -> int:
|
|
332
348
|
from memgraph_context_graph.hooks.runtime_plugin import load_runtime_plugins
|
|
333
349
|
|
|
@@ -12,6 +12,7 @@ import io
|
|
|
12
12
|
import json
|
|
13
13
|
import shutil
|
|
14
14
|
import subprocess
|
|
15
|
+
import time
|
|
15
16
|
from dataclasses import dataclass
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import TYPE_CHECKING, Literal
|
|
@@ -90,6 +91,7 @@ def wire_plugin(agent: Agent, log: Callable[[str], None]) -> str:
|
|
|
90
91
|
"""
|
|
91
92
|
binary = _require(agent)
|
|
92
93
|
if plugin_wired(agent):
|
|
94
|
+
_allow_recall(agent)
|
|
93
95
|
return "plugin already installed"
|
|
94
96
|
if not _marketplace_registered(agent, binary):
|
|
95
97
|
_stream([binary, "plugin", "marketplace", "add", MARKETPLACE_SOURCE, "--sparse", agent.sparse], log)
|
|
@@ -97,6 +99,7 @@ def wire_plugin(agent: Agent, log: Callable[[str], None]) -> str:
|
|
|
97
99
|
_stream([binary, "plugin", "install", PLUGIN_ID, "-y"], log)
|
|
98
100
|
else:
|
|
99
101
|
_stream([binary, "plugin", "add", PLUGIN_ID], log)
|
|
102
|
+
_allow_recall(agent)
|
|
100
103
|
return "plugin installed"
|
|
101
104
|
|
|
102
105
|
|
|
@@ -112,12 +115,26 @@ def unwire_plugin(agent: Agent, log: Callable[[str], None]) -> str:
|
|
|
112
115
|
verb = "uninstall" if agent.runtime == "claude-code" else "remove"
|
|
113
116
|
_stream([binary, "plugin", verb, PLUGIN_ID], log)
|
|
114
117
|
removed.append("plugin")
|
|
118
|
+
if agent.runtime == "codex":
|
|
119
|
+
_codex_memory().disallow_plugin_tools(Path.home(), PLUGIN_ID)
|
|
115
120
|
if _marketplace_registered(agent, binary):
|
|
116
121
|
_stream([binary, "plugin", "marketplace", "remove", MARKETPLACE], log)
|
|
117
122
|
removed.append("marketplace")
|
|
118
123
|
return f"removed the {' and '.join(removed)}" if removed else "wasn't installed"
|
|
119
124
|
|
|
120
125
|
|
|
126
|
+
def _allow_recall(agent: Agent) -> None:
|
|
127
|
+
"""Codex refuses an MCP tool that needs approval when its policy is ``never``; recall is read-only, so allow it."""
|
|
128
|
+
if agent.runtime == "codex":
|
|
129
|
+
_codex_memory().allow_plugin_tools(Path.home(), PLUGIN_ID)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _codex_memory():
|
|
133
|
+
from memgraph_context_graph.adapters import codex_memory
|
|
134
|
+
|
|
135
|
+
return codex_memory
|
|
136
|
+
|
|
137
|
+
|
|
121
138
|
def _marketplace_registered(agent: Agent, binary: str) -> bool:
|
|
122
139
|
listing = _json([binary, "plugin", "marketplace", "list", "--json"])
|
|
123
140
|
entries = listing.get("marketplaces", []) if isinstance(listing, dict) else listing or []
|
|
@@ -220,11 +237,26 @@ def _require(agent: Agent) -> str:
|
|
|
220
237
|
|
|
221
238
|
|
|
222
239
|
def _json(argv: list[str]):
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
"""What an agent CLI's ``--json`` listing printed, or None when it didn't print one.
|
|
241
|
+
|
|
242
|
+
A listing that fails once (the CLI busy updating itself, a notice printed ahead of the JSON) would read as
|
|
243
|
+
"not installed" and mislabel a wired agent, so it is asked twice and the JSON is taken from where it starts.
|
|
244
|
+
"""
|
|
245
|
+
for attempt in range(2):
|
|
246
|
+
if attempt:
|
|
247
|
+
time.sleep(0.5)
|
|
248
|
+
result = subprocess.run(argv, capture_output=True, text=True, check=False)
|
|
249
|
+
if result.returncode == 0 and (found := _json_in(result.stdout)) is not None:
|
|
250
|
+
return found
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _json_in(text: str):
|
|
255
|
+
start = min((i for i in (text.find("["), text.find("{")) if i >= 0), default=-1)
|
|
256
|
+
if start < 0:
|
|
225
257
|
return None
|
|
226
258
|
try:
|
|
227
|
-
return json.loads(
|
|
259
|
+
return json.loads(text[start:])
|
|
228
260
|
except json.JSONDecodeError:
|
|
229
261
|
return None
|
|
230
262
|
|
|
@@ -90,7 +90,7 @@ def setup_main(argv: Sequence[str]) -> int:
|
|
|
90
90
|
_print_plan(choices, env)
|
|
91
91
|
print()
|
|
92
92
|
outcomes = engine.run(choices, env, log=_log, report=_report)
|
|
93
|
-
return _finish(outcomes)
|
|
93
|
+
return _finish(outcomes, choices, env)
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def uninstall_main(argv: Sequence[str]) -> int:
|
|
@@ -141,11 +141,16 @@ def _report(outcome: engine.Outcome) -> None:
|
|
|
141
141
|
print(f" {line}")
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
def _finish(outcomes: Sequence[engine.Outcome]) -> int:
|
|
144
|
+
def _finish(outcomes: Sequence[engine.Outcome], choices: engine.Choices, env: engine.Environment) -> int:
|
|
145
145
|
failures = [outcome for outcome in outcomes if outcome.status == "fail"]
|
|
146
146
|
print()
|
|
147
147
|
if not failures:
|
|
148
|
-
print("Context Graph is set up.
|
|
148
|
+
print("Context Graph is set up.")
|
|
149
|
+
print()
|
|
150
|
+
steps = engine.next_steps(choices, env)
|
|
151
|
+
print(steps[0])
|
|
152
|
+
for line in steps[1:]:
|
|
153
|
+
print(f" {line}")
|
|
149
154
|
return 0
|
|
150
155
|
print(f"{len(failures)} step(s) failed. To fix:")
|
|
151
156
|
for outcome in failures:
|
|
@@ -269,11 +269,7 @@ def _agent(agent: agents_mod.Agent, choices: Choices, log: Callable[[str], None]
|
|
|
269
269
|
fix = f"memgraph-context-graph setup {agent.runtime}"
|
|
270
270
|
if agent.wiring == "plugin":
|
|
271
271
|
try:
|
|
272
|
-
|
|
273
|
-
if agent.runtime == "codex":
|
|
274
|
-
# Codex skips an untrusted plugin's hooks without a word, and `codex exec` never asks.
|
|
275
|
-
summary += "; Codex records nothing until you trust its hooks: open codex and accept them (/hooks)"
|
|
276
|
-
return Outcome(agent.label, "ok", summary)
|
|
272
|
+
return Outcome(agent.label, "ok", _with_trust_note(agent, agents_mod.wire_plugin(agent, log)))
|
|
277
273
|
except agents_mod.WiringError as exc:
|
|
278
274
|
return Outcome(agent.label, "fail", str(exc), fix=fix)
|
|
279
275
|
if not choices.project_dirs:
|
|
@@ -287,7 +283,65 @@ def _agent(agent: agents_mod.Agent, choices: Choices, log: Callable[[str], None]
|
|
|
287
283
|
if errors:
|
|
288
284
|
dirs = " ".join(f"--project-dir {path}" for path in choices.project_dirs)
|
|
289
285
|
return Outcome(agent.label, "fail", "; ".join(errors + summaries), fix=f"{fix} {dirs}")
|
|
290
|
-
return Outcome(agent.label, "ok", "; ".join(summaries))
|
|
286
|
+
return Outcome(agent.label, "ok", _with_trust_note(agent, "; ".join(summaries)))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
#: Agents that skip hooks they haven't been told to trust, without a word: nothing is recorded until then.
|
|
290
|
+
_TRUST_NOTES = {
|
|
291
|
+
"codex": "Codex records nothing until you trust its hooks: open codex and accept them (/hooks)",
|
|
292
|
+
"grok": "Grok records nothing until you trust this project's hooks: run /hooks-trust in grok",
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _with_trust_note(agent: agents_mod.Agent, summary: str) -> str:
|
|
297
|
+
note = _TRUST_NOTES.get(agent.runtime)
|
|
298
|
+
return f"{summary}; {note}" if note else summary
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
#: What setup says before it asks anything: what the user is about to get, in plain words.
|
|
302
|
+
WELCOME = (
|
|
303
|
+
"Your coding agents are about to get a memory.",
|
|
304
|
+
"Each session is kept as one graph in Memgraph, on your machine by default:",
|
|
305
|
+
"prompts, replies and tool activity. No account or cloud needed.",
|
|
306
|
+
"A few questions, then setup does the rest. Nothing changes until you confirm.",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
#: What a user does once, so an agent starts recording; the same facts as :data:`_TRUST_NOTES`, for the last screen.
|
|
310
|
+
_FIRST_RUN = {
|
|
311
|
+
"codex": "in Codex, open it and accept its hooks (/hooks)",
|
|
312
|
+
"grok": "in Grok, run /hooks-trust once per wired project",
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def next_steps(choices: Choices, env: Environment) -> list[str]:
|
|
317
|
+
"""What happens now and what to try, for the end of a successful setup: short, and only what applies.
|
|
318
|
+
|
|
319
|
+
The first line is the heading; each bullet after it fits one terminal line, or continues on an indented one.
|
|
320
|
+
"""
|
|
321
|
+
from memgraph_context_graph.adapters._identity import MEMORY_BACKEND_GRAPH
|
|
322
|
+
|
|
323
|
+
graph_memory = choices.memory_backend == MEMORY_BACKEND_GRAPH or (
|
|
324
|
+
choices.memory_backend is None and env.configured_memory_backend == MEMORY_BACKEND_GRAPH
|
|
325
|
+
)
|
|
326
|
+
lines = [
|
|
327
|
+
"Your agents now remember. What to expect:",
|
|
328
|
+
"· Every session is kept in your graph. Finish one, start another, and ask:",
|
|
329
|
+
' "what did we do last time?"',
|
|
330
|
+
]
|
|
331
|
+
remembering = [agents_mod.BY_RUNTIME[r].label for r in choices.runtimes if has_native_memory(r)]
|
|
332
|
+
if graph_memory and remembering:
|
|
333
|
+
lines += [
|
|
334
|
+
f"· {' and '.join(remembering)} remember through the graph now.",
|
|
335
|
+
' Say "remember that ..." and every agent can recall it.',
|
|
336
|
+
]
|
|
337
|
+
if choices.viewer:
|
|
338
|
+
lines.append("· Watch it grow: memgraph-context-graph ui")
|
|
339
|
+
lines += [f"· To start recording {_FIRST_RUN[r]}" for r in choices.runtimes if r in _FIRST_RUN]
|
|
340
|
+
lines += [
|
|
341
|
+
"· Change anything any time: memgraph-context-graph setup",
|
|
342
|
+
"· Undo the wiring: memgraph-context-graph uninstall (your graph stays)",
|
|
343
|
+
]
|
|
344
|
+
return lines
|
|
291
345
|
|
|
292
346
|
|
|
293
347
|
def _memory(runtime: str, backend: str) -> Outcome | None:
|
|
@@ -223,13 +223,14 @@ class Prompts:
|
|
|
223
223
|
self.write("\n" + "".join(f"{self.style.wordmark(row)}\n" for row in WORDMARK))
|
|
224
224
|
self.write("".join(f"{self.style.graph(row, width)}\n" for row in GRAPH) + "\n")
|
|
225
225
|
|
|
226
|
-
def intro(self, title: str, subtitle: str = "") -> None:
|
|
227
|
-
"""The orange graph divider, some space, then the title: the questions start apart from what came before."""
|
|
226
|
+
def intro(self, title: str, subtitle: str | Sequence[str] = "") -> None:
|
|
227
|
+
"""The orange graph divider, some space, then the title and its lines: the questions start apart from what came before."""
|
|
228
228
|
rule = RULE[: min(len(WORDMARK[0]), self.term.columns - 1)]
|
|
229
229
|
rule = rule[: rule.rfind("●") + 1] # cut short, it still ends on a node
|
|
230
230
|
self.write(f"\n{self.style.accent(rule)}\n\n")
|
|
231
231
|
self.write(f"{self.style.accent('◆')} {self.style.bold(title)}\n")
|
|
232
|
-
|
|
232
|
+
for text in [subtitle] if isinstance(subtitle, str) else subtitle:
|
|
233
|
+
self.line(self.style.muted(self.fit(text)))
|
|
233
234
|
|
|
234
235
|
def answered(self, label: str, value: str) -> None:
|
|
235
236
|
value = self.fit(value, _ANSWER_COLUMN)
|
|
@@ -400,7 +401,7 @@ def ask(prompts: Prompts, choices: Choices, env: Environment, *, preselect: bool
|
|
|
400
401
|
|
|
401
402
|
*preselect* starts the agent list with *choices*' agents ticked; off, the user ticks every one.
|
|
402
403
|
"""
|
|
403
|
-
prompts.intro("Context Graph setup",
|
|
404
|
+
prompts.intro("Context Graph setup", engine.WELCOME)
|
|
404
405
|
enterprise = env.edition == "enterprise"
|
|
405
406
|
prompts.answered("Memgraph", "from your Enterprise login" if enterprise else _memgraph_summary(choices, env))
|
|
406
407
|
|
|
@@ -418,7 +419,7 @@ def ask(prompts: Prompts, choices: Choices, env: Environment, *, preselect: bool
|
|
|
418
419
|
def name(answers: _Answers) -> str | _Back:
|
|
419
420
|
if enterprise:
|
|
420
421
|
return SKIPPED
|
|
421
|
-
answer = prompts.text("Your name", answers.user_id, hint="
|
|
422
|
+
answer = prompts.text("Your name", answers.user_id, hint="your memories are filed under it")
|
|
422
423
|
if isinstance(answer, str):
|
|
423
424
|
answers.user_id = answer
|
|
424
425
|
return answer if isinstance(answer, _Back) else DONE
|
|
@@ -444,7 +445,7 @@ def ask(prompts: Prompts, choices: Choices, env: Environment, *, preselect: bool
|
|
|
444
445
|
answer = prompts.confirm(
|
|
445
446
|
"Install the terminal viewer?",
|
|
446
447
|
answers.viewer,
|
|
447
|
-
note="
|
|
448
|
+
note="Watch your agents' memory grow, right in this terminal: memgraph-context-graph ui.",
|
|
448
449
|
short="Viewer",
|
|
449
450
|
)
|
|
450
451
|
if isinstance(answer, bool):
|
|
@@ -459,7 +460,7 @@ def ask(prompts: Prompts, choices: Choices, env: Environment, *, preselect: bool
|
|
|
459
460
|
answer = prompts.confirm(
|
|
460
461
|
f"Use Context Graph as the memory of {' and '.join(switchable)}?",
|
|
461
462
|
answers.use_graph_memory,
|
|
462
|
-
note="Their
|
|
463
|
+
note="Their memories move into the graph, shared across agents. Switch back any time.",
|
|
463
464
|
short="Agent memory",
|
|
464
465
|
)
|
|
465
466
|
if isinstance(answer, bool):
|
|
@@ -524,7 +525,7 @@ def run(prompts: Prompts, choices: Choices, env: Environment) -> int:
|
|
|
524
525
|
style = prompts.style
|
|
525
526
|
marks = {"ok": style.success("✓"), "skip": style.muted("–"), "warn": style.warning("!"), "fail": style.error("✗")}
|
|
526
527
|
prompts.line()
|
|
527
|
-
prompts.line(style.muted("
|
|
528
|
+
prompts.line(style.muted("Setting up… (a first Memgraph start downloads about 1 GB)"))
|
|
528
529
|
|
|
529
530
|
def log(text: str) -> None:
|
|
530
531
|
prompts.line(style.muted(f" {text}"))
|
|
@@ -537,10 +538,12 @@ def run(prompts: Prompts, choices: Choices, env: Environment) -> int:
|
|
|
537
538
|
outcomes = engine.run(choices, env, log=log, report=report)
|
|
538
539
|
failures = [outcome for outcome in outcomes if outcome.status == "fail"]
|
|
539
540
|
if not failures:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
541
|
+
steps = engine.next_steps(choices, env)
|
|
542
|
+
prompts.line()
|
|
543
|
+
prompts.line(style.bold(steps[0]))
|
|
544
|
+
for text in steps[1:]:
|
|
545
|
+
prompts.line(prompts.fit(text))
|
|
546
|
+
prompts.outro(style.success("Context Graph is set up."))
|
|
544
547
|
return 0
|
|
545
548
|
prompts.line()
|
|
546
549
|
prompts.line(style.error(f"{len(failures)} step(s) failed. To fix:"))
|
|
@@ -577,7 +580,7 @@ def _memgraph_summary(choices: Choices, env: Environment) -> str:
|
|
|
577
580
|
return choices.memgraph_url
|
|
578
581
|
if env.configured_url:
|
|
579
582
|
return f"{env.configured_url} (configured)"
|
|
580
|
-
return "
|
|
583
|
+
return "uses Memgraph on localhost:7687, or starts one in Docker for you"
|
|
581
584
|
|
|
582
585
|
|
|
583
586
|
def _agent_options(selected: Sequence[str], wired: Sequence[str]) -> list[_Option]:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import importlib
|
|
6
|
+
import shlex
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
8
9
|
import sys
|
|
@@ -14,13 +15,14 @@ if TYPE_CHECKING:
|
|
|
14
15
|
from collections.abc import Callable
|
|
15
16
|
|
|
16
17
|
#: Kept in step with the plugins' bootstrap scripts and install.sh, which install the same set first.
|
|
17
|
-
BASE = "memgraph-context-graph[mcp]>=0.7.
|
|
18
|
+
BASE = "memgraph-context-graph[mcp]>=0.7.3"
|
|
18
19
|
CONNECTORS = {
|
|
19
20
|
"skills-graph": "skills-graph[memgraph-context-graph]>=0.2.1",
|
|
20
21
|
"actions-graph": "actions-graph[memgraph-context-graph]>=0.5.0",
|
|
21
22
|
"sessions-graph": "sessions-graph[memgraph-context-graph]>=0.8.1",
|
|
22
23
|
}
|
|
23
24
|
VIEWER = ("context-graph-viewer", "context-graph-viewer>=0.1.0")
|
|
25
|
+
SPACES = ("context-graph-spaces", "context-graph-spaces>=0.1.0")
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
class ToolError(Exception):
|
|
@@ -59,8 +61,29 @@ def uv_tool_environment() -> bool:
|
|
|
59
61
|
return Path(sys.prefix).resolve() == tool_dir.resolve()
|
|
60
62
|
|
|
61
63
|
|
|
62
|
-
def
|
|
63
|
-
"""
|
|
64
|
+
def requirements(*, viewer: bool = False, spaces: bool = False) -> list[str]:
|
|
65
|
+
"""The ``uv tool install`` arguments for the tool, every connector, and the optional packages.
|
|
66
|
+
|
|
67
|
+
An optional package already installed is kept: ``uv tool install`` replaces the tool's requirements with
|
|
68
|
+
what it is given, so leaving one out would remove it.
|
|
69
|
+
"""
|
|
70
|
+
argv = [BASE]
|
|
71
|
+
for requirement in CONNECTORS.values():
|
|
72
|
+
argv += ["--with", requirement]
|
|
73
|
+
if viewer or installed(VIEWER[0]):
|
|
74
|
+
argv += ["--with", VIEWER[1]]
|
|
75
|
+
if spaces or installed(SPACES[0]):
|
|
76
|
+
argv += ["--with", SPACES[1]]
|
|
77
|
+
return argv
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def install_command(*, spaces: bool = False) -> str:
|
|
81
|
+
"""The whole ``uv tool install`` command as one line a person can paste, with *spaces* added."""
|
|
82
|
+
return " ".join(["uv tool install", *map(shlex.quote, requirements(spaces=spaces))])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def install(*, viewer: bool, log: Callable[[str], None], spaces: bool = False) -> None:
|
|
86
|
+
"""Install the tool with every connector (and the viewer, and the spaces package), keeping what is already there.
|
|
64
87
|
|
|
65
88
|
The full set is always passed: ``uv tool install`` replaces the tool's requirements with what it is
|
|
66
89
|
given, so naming only the missing package would drop the others.
|
|
@@ -71,11 +94,7 @@ def install(*, viewer: bool, log: Callable[[str], None]) -> None:
|
|
|
71
94
|
uv = shutil.which("uv")
|
|
72
95
|
if uv is None:
|
|
73
96
|
raise ToolError("uv isn't on PATH")
|
|
74
|
-
argv = [uv, "--color", "never", "tool", "install",
|
|
75
|
-
for requirement in CONNECTORS.values():
|
|
76
|
-
argv += ["--with", requirement]
|
|
77
|
-
if viewer or installed(VIEWER[0]):
|
|
78
|
-
argv += ["--with", VIEWER[1]]
|
|
97
|
+
argv = [uv, "--color", "never", "tool", "install", *requirements(viewer=viewer, spaces=spaces)]
|
|
79
98
|
log(" ".join(["uv", *argv[3:]]))
|
|
80
99
|
process = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
81
100
|
last = ""
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/spaces.py
RENAMED
|
@@ -192,9 +192,11 @@ def _build(config: HookConfig) -> SpaceResolver:
|
|
|
192
192
|
if edition == EDITION_ENTERPRISE:
|
|
193
193
|
factory = _edition_factory(edition)
|
|
194
194
|
if factory is None:
|
|
195
|
+
from memgraph_context_graph.setup import tool
|
|
196
|
+
|
|
195
197
|
raise SpaceError(
|
|
196
|
-
"edition = enterprise needs the context-graph-spaces package: "
|
|
197
|
-
"
|
|
198
|
+
f"edition = enterprise needs the context-graph-spaces package. Install it, with the rest: "
|
|
199
|
+
f"{tool.install_command(spaces=True)}"
|
|
198
200
|
)
|
|
199
201
|
return factory(config)
|
|
200
202
|
raise SpaceError(f"Unknown edition {edition!r} in the config file; use one of: {', '.join(sorted(EDITIONS))}.")
|
|
@@ -107,6 +107,6 @@ def test_installer_installs_the_tool_then_hands_its_arguments_to_setup(tmp_path)
|
|
|
107
107
|
) # fmt: skip
|
|
108
108
|
|
|
109
109
|
lines = calls.read_text().splitlines()
|
|
110
|
-
assert lines[0].startswith("uv tool install memgraph-context-graph[mcp]>=")
|
|
110
|
+
assert lines[0].startswith("uv tool install --quiet memgraph-context-graph[mcp]>=")
|
|
111
111
|
assert "--with sessions-graph[memgraph-context-graph]>=" in lines[0]
|
|
112
112
|
assert lines[-1] == "memgraph-context-graph setup --no-tui codex --memgraph-url bolt://my-host:7687"
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import contextlib
|
|
10
10
|
import json
|
|
11
11
|
import re
|
|
12
|
+
import subprocess
|
|
12
13
|
|
|
13
14
|
import pytest
|
|
14
15
|
|
|
@@ -440,6 +441,16 @@ def test_the_uv_tool_is_recognised_even_when_the_shell_forces_colour(monkeypatch
|
|
|
440
441
|
assert tool.uv_tool_environment()
|
|
441
442
|
|
|
442
443
|
|
|
444
|
+
def test_grok_is_reported_with_the_trust_it_still_needs(fake_agents, setup_home, project):
|
|
445
|
+
env = engine.look_around(cwd=project)
|
|
446
|
+
choices = engine.default_choices(env, cwd=project)
|
|
447
|
+
|
|
448
|
+
grok = engine._agent(BY_RUNTIME["grok"], choices, lambda _: None)
|
|
449
|
+
|
|
450
|
+
assert grok.status == "ok"
|
|
451
|
+
assert "/hooks-trust" in grok.summary
|
|
452
|
+
|
|
453
|
+
|
|
443
454
|
def test_codex_is_reported_with_the_trust_it_still_needs_and_claude_code_is_not(fake_agents, setup_home, project):
|
|
444
455
|
env = engine.look_around(cwd=project)
|
|
445
456
|
choices = engine.default_choices(env, cwd=project)
|
|
@@ -450,3 +461,158 @@ def test_codex_is_reported_with_the_trust_it_still_needs_and_claude_code_is_not(
|
|
|
450
461
|
assert codex.status == "ok"
|
|
451
462
|
assert "records nothing until you trust its hooks" in codex.summary
|
|
452
463
|
assert "trust" not in claude.summary
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def test_reinstalling_the_tool_keeps_the_spaces_package_once_it_is_there(monkeypatch):
|
|
467
|
+
from memgraph_context_graph.setup import tool
|
|
468
|
+
|
|
469
|
+
monkeypatch.setattr(tool, "installed", lambda package: package == "context-graph-spaces")
|
|
470
|
+
|
|
471
|
+
argv = tool.requirements()
|
|
472
|
+
|
|
473
|
+
assert tool.BASE in argv
|
|
474
|
+
assert all(requirement in argv for requirement in tool.CONNECTORS.values())
|
|
475
|
+
assert tool.SPACES[1] in argv
|
|
476
|
+
assert tool.VIEWER[1] not in argv
|
|
477
|
+
|
|
478
|
+
monkeypatch.setattr(tool, "installed", lambda package: False)
|
|
479
|
+
assert tool.SPACES[1] not in tool.requirements()
|
|
480
|
+
assert tool.SPACES[1] in tool.requirements(spaces=True)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def test_the_enterprise_hint_names_the_whole_install_not_just_the_spaces_package(monkeypatch):
|
|
484
|
+
import pytest
|
|
485
|
+
|
|
486
|
+
from memgraph_context_graph import spaces
|
|
487
|
+
from memgraph_context_graph.adapters._identity import HookConfig
|
|
488
|
+
|
|
489
|
+
monkeypatch.setattr(spaces, "_edition_factory", lambda edition: None)
|
|
490
|
+
spaces._reset_cache()
|
|
491
|
+
|
|
492
|
+
with pytest.raises(spaces.SpaceError) as excinfo:
|
|
493
|
+
spaces.spaces_for(HookConfig(edition="enterprise"))
|
|
494
|
+
|
|
495
|
+
hint = str(excinfo.value)
|
|
496
|
+
assert "context-graph-spaces" in hint
|
|
497
|
+
assert "skills-graph" in hint and "sessions-graph" in hint
|
|
498
|
+
spaces._reset_cache()
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def test_join_installs_the_spaces_package_inside_the_uv_tool_and_not_elsewhere(monkeypatch, setup_home, capsys):
|
|
502
|
+
from memgraph_context_graph import spaces
|
|
503
|
+
from memgraph_context_graph.setup import tool
|
|
504
|
+
|
|
505
|
+
installs = []
|
|
506
|
+
monkeypatch.setattr(spaces, "_edition_factory", lambda edition: None)
|
|
507
|
+
monkeypatch.setattr(tool, "install", lambda **kwargs: installs.append(kwargs))
|
|
508
|
+
|
|
509
|
+
monkeypatch.setattr(tool, "uv_tool_environment", lambda: False)
|
|
510
|
+
spaces._reset_cache()
|
|
511
|
+
assert main(["join", "bolt://localhost:1", "ante", "--password=x"]) == 1
|
|
512
|
+
assert installs == []
|
|
513
|
+
assert "uv tool install" in capsys.readouterr().err
|
|
514
|
+
|
|
515
|
+
monkeypatch.setattr(tool, "uv_tool_environment", lambda: True)
|
|
516
|
+
spaces._reset_cache()
|
|
517
|
+
main(["join", "bolt://localhost:1", "ante", "--password=x"])
|
|
518
|
+
assert [kwargs["spaces"] for kwargs in installs] == [True]
|
|
519
|
+
spaces._reset_cache()
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def test_the_welcome_says_what_the_user_is_getting_before_any_question(fake_agents, setup_home, project):
|
|
523
|
+
from memgraph_context_graph.setup.prompts import ENTER, Prompts, ask
|
|
524
|
+
|
|
525
|
+
env = engine.look_around(cwd=project)
|
|
526
|
+
term = ScriptedTerminal(keys=[ENTER] * 5, columns=100)
|
|
527
|
+
|
|
528
|
+
ask(Prompts(term), engine.default_choices(env, cwd=project), env, preselect=False)
|
|
529
|
+
|
|
530
|
+
first_question = term.text.index("Your name")
|
|
531
|
+
for line in ("about to get a memory", "No account or cloud needed", "Nothing changes until you confirm"):
|
|
532
|
+
assert term.text.index(line) < first_question
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def test_the_last_screen_says_what_to_expect_and_only_what_applies():
|
|
536
|
+
choices = engine.Choices(
|
|
537
|
+
runtimes=["claude-code", "codex", "grok"], project_dirs=[], user_id="carol", memory_backend="context-graph"
|
|
538
|
+
)
|
|
539
|
+
env = engine.Environment(edition="community", configured_url=None, configured_user=None, found_agents=())
|
|
540
|
+
|
|
541
|
+
text = "\n".join(engine.next_steps(choices, env))
|
|
542
|
+
|
|
543
|
+
assert "Claude Code and Codex remember through the graph now." in text
|
|
544
|
+
assert "memgraph-context-graph ui" in text
|
|
545
|
+
assert "in Codex, open it and accept its hooks" in text
|
|
546
|
+
assert "in Grok, run /hooks-trust" in text
|
|
547
|
+
assert "memgraph-context-graph uninstall" in text
|
|
548
|
+
|
|
549
|
+
plain = engine.Choices(runtimes=["opencode"], project_dirs=[], user_id="carol", viewer=False)
|
|
550
|
+
quiet = "\n".join(engine.next_steps(plain, env))
|
|
551
|
+
|
|
552
|
+
assert "remember through the graph" not in quiet
|
|
553
|
+
assert "memgraph-context-graph ui" not in quiet
|
|
554
|
+
assert "accept the hooks" not in quiet
|
|
555
|
+
assert "what did we do last time" in quiet
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def test_every_line_of_the_welcome_and_the_last_screen_fits_a_narrow_terminal():
|
|
559
|
+
choices = engine.Choices(
|
|
560
|
+
runtimes=["claude-code", "codex", "grok"], project_dirs=[], user_id="carol", memory_backend="context-graph"
|
|
561
|
+
)
|
|
562
|
+
env = engine.Environment(edition="community", configured_url=None, configured_user=None, found_agents=())
|
|
563
|
+
|
|
564
|
+
for line in (*engine.WELCOME, *engine.next_steps(choices, env)):
|
|
565
|
+
assert len(line) <= 78, line
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def test_a_listing_that_fails_once_does_not_make_a_wired_agent_look_unwired(monkeypatch):
|
|
569
|
+
listing = json.dumps([{"id": agents.PLUGIN_ID}])
|
|
570
|
+
replies = iter(
|
|
571
|
+
[
|
|
572
|
+
subprocess.CompletedProcess([], 1, "", "busy"),
|
|
573
|
+
subprocess.CompletedProcess([], 0, f"Checking for updates...\n{listing}", ""),
|
|
574
|
+
]
|
|
575
|
+
)
|
|
576
|
+
monkeypatch.setattr(agents.subprocess, "run", lambda *a, **k: next(replies))
|
|
577
|
+
monkeypatch.setattr(agents.time, "sleep", lambda _: None)
|
|
578
|
+
monkeypatch.setattr(agents.shutil, "which", lambda name: f"/bin/{name}")
|
|
579
|
+
|
|
580
|
+
assert agents.plugin_wired(BY_RUNTIME["claude-code"])
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def test_codex_wiring_lets_recall_through_a_never_policy_and_unwiring_takes_it_back(fake_agents, setup_home):
|
|
584
|
+
import tomlkit
|
|
585
|
+
|
|
586
|
+
config = setup_home / ".codex" / "config.toml"
|
|
587
|
+
config.parent.mkdir()
|
|
588
|
+
config.write_text('approval_policy = "never"\n\n[features]\nhooks = true\n')
|
|
589
|
+
codex = BY_RUNTIME["codex"]
|
|
590
|
+
|
|
591
|
+
agents.wire_plugin(codex, lambda _: None)
|
|
592
|
+
agents.wire_plugin(codex, lambda _: None)
|
|
593
|
+
|
|
594
|
+
parsed = tomlkit.parse(config.read_text()).unwrap()
|
|
595
|
+
assert parsed["plugins"][agents.PLUGIN_ID]["mcp_servers"]["context-graph"] == {
|
|
596
|
+
"default_tools_approval_mode": "approve"
|
|
597
|
+
}
|
|
598
|
+
assert parsed["approval_policy"] == "never" and parsed["features"] == {"hooks": True}
|
|
599
|
+
|
|
600
|
+
agents.unwire_plugin(codex, lambda _: None)
|
|
601
|
+
|
|
602
|
+
assert tomlkit.parse(config.read_text()).unwrap() == {"approval_policy": "never", "features": {"hooks": True}}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def test_codex_wiring_keeps_an_approval_mode_the_user_chose(fake_agents, setup_home):
|
|
606
|
+
import tomlkit
|
|
607
|
+
|
|
608
|
+
config = setup_home / ".codex" / "config.toml"
|
|
609
|
+
config.parent.mkdir()
|
|
610
|
+
config.write_text(
|
|
611
|
+
f'[plugins."{agents.PLUGIN_ID}".mcp_servers.context-graph]\ndefault_tools_approval_mode = "prompt"\n'
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
agents.wire_plugin(BY_RUNTIME["codex"], lambda _: None)
|
|
615
|
+
agents.unwire_plugin(BY_RUNTIME["codex"], lambda _: None)
|
|
616
|
+
|
|
617
|
+
mode = tomlkit.parse(config.read_text()).unwrap()["plugins"][agents.PLUGIN_ID]["mcp_servers"]["context-graph"]
|
|
618
|
+
assert mode == {"default_tools_approval_mode": "prompt"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/adr/0003-config-path-override.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/docs/research/2026-10-codex-memory.md
RENAMED
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/link.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/tools.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_antigravity_cli_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_claude_code_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_context_before_tool.py
RENAMED
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_copilot_cli_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.4}/tests/test_opencode_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|