memgraph-context-graph 0.7.3__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.
Files changed (76) hide show
  1. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/PKG-INFO +1 -1
  2. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/pyproject.toml +1 -1
  3. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/codex_memory.py +52 -0
  4. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/agents.py +35 -3
  5. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/tool.py +1 -1
  6. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_setup.py +54 -0
  7. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/.gitignore +0 -0
  8. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/CONTEXT.md +0 -0
  9. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/LICENSE +0 -0
  10. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/README.md +0 -0
  11. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
  12. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
  13. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/docs/adr/0003-config-path-override.md +0 -0
  14. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
  15. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
  16. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
  17. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/docs/command-hooks.md +0 -0
  18. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/docs/research/2026-10-codex-memory.md +0 -0
  19. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/__init__.py +0 -0
  20. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/__init__.py +0 -0
  21. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/_identity.py +0 -0
  22. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
  23. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/_spec.py +0 -0
  24. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/antigravity_cli.py +0 -0
  25. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/claude.py +0 -0
  26. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/claude_code.py +0 -0
  27. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/claude_code_memory.py +0 -0
  28. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/codex.py +0 -0
  29. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/copilot_cli.py +0 -0
  30. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/cursor.py +0 -0
  31. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/grok.py +0 -0
  32. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/openai.py +0 -0
  33. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/adapters/opencode.py +0 -0
  34. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/capture_log.py +0 -0
  35. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/cli.py +0 -0
  36. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/events.py +0 -0
  37. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/hooks/__init__.py +0 -0
  38. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/hooks/cli.py +0 -0
  39. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/hooks/runner.py +0 -0
  40. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/hooks/runtime_plugin.py +0 -0
  41. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/link.py +0 -0
  42. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/mcp_server.py +0 -0
  43. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/memory_backend.py +0 -0
  44. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/protocols.py +0 -0
  45. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/__init__.py +0 -0
  46. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/cli.py +0 -0
  47. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/engine.py +0 -0
  48. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/memgraph.py +0 -0
  49. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/prompts.py +0 -0
  50. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/setup/uninstall.py +0 -0
  51. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/spaces.py +0 -0
  52. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/src/memgraph_context_graph/tools.py +0 -0
  53. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/__init__.py +0 -0
  54. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/conftest.py +0 -0
  55. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/fake_agent_cli.py +0 -0
  56. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_antigravity_cli_adapter.py +0 -0
  57. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_capture_log.py +0 -0
  58. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_claude_code_adapter.py +0 -0
  59. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_codex_adapter.py +0 -0
  60. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_config_cli.py +0 -0
  61. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_context_before_tool.py +0 -0
  62. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_copilot_cli_adapter.py +0 -0
  63. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_cursor_adapter.py +0 -0
  64. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_dev_scripts.py +0 -0
  65. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_events.py +0 -0
  66. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_grok_adapter.py +0 -0
  67. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_hook_cli.py +0 -0
  68. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_identity.py +0 -0
  69. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_link.py +0 -0
  70. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_opencode_adapter.py +0 -0
  71. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_plugin_hooks.py +0 -0
  72. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_runner_sessions_graph_connector.py +0 -0
  73. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_runtime_plugin.py +0 -0
  74. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_setup_docker.py +0 -0
  75. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_spaces.py +0 -0
  76. {memgraph_context_graph-0.7.3 → memgraph_context_graph-0.7.4}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memgraph-context-graph
3
- Version: 0.7.3
3
+ Version: 0.7.4
4
4
  Summary: Connect agent SDKs to context-graph components (actions-graph, skills-graph, etc.)
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memgraph-context-graph"
3
- version = "0.7.3"
3
+ version = "0.7.4"
4
4
  description = "Connect agent SDKs to context-graph components (actions-graph, skills-graph, etc.)"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -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"
@@ -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
- result = subprocess.run(argv, capture_output=True, text=True, check=False)
224
- if result.returncode != 0:
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(result.stdout)
259
+ return json.loads(text[start:])
228
260
  except json.JSONDecodeError:
229
261
  return None
230
262
 
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  from collections.abc import Callable
16
16
 
17
17
  #: Kept in step with the plugins' bootstrap scripts and install.sh, which install the same set first.
18
- BASE = "memgraph-context-graph[mcp]>=0.7.2"
18
+ BASE = "memgraph-context-graph[mcp]>=0.7.3"
19
19
  CONNECTORS = {
20
20
  "skills-graph": "skills-graph[memgraph-context-graph]>=0.2.1",
21
21
  "actions-graph": "actions-graph[memgraph-context-graph]>=0.5.0",
@@ -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
 
@@ -562,3 +563,56 @@ def test_every_line_of_the_welcome_and_the_last_screen_fits_a_narrow_terminal():
562
563
 
563
564
  for line in (*engine.WELCOME, *engine.next_steps(choices, env)):
564
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"}