memgraph-context-graph 0.7.2__tar.gz → 0.7.3__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.2 → memgraph_context_graph-0.7.3}/PKG-INFO +1 -1
  2. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/pyproject.toml +1 -1
  3. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/cli.py +19 -3
  4. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/setup/cli.py +8 -3
  5. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/setup/engine.py +60 -6
  6. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/setup/prompts.py +16 -13
  7. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/setup/tool.py +27 -8
  8. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/spaces.py +4 -2
  9. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_dev_scripts.py +1 -1
  10. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_setup.py +112 -0
  11. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/.gitignore +0 -0
  12. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/CONTEXT.md +0 -0
  13. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/LICENSE +0 -0
  14. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/README.md +0 -0
  15. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
  16. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
  17. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/docs/adr/0003-config-path-override.md +0 -0
  18. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
  19. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
  20. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
  21. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/docs/command-hooks.md +0 -0
  22. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/docs/research/2026-10-codex-memory.md +0 -0
  23. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/__init__.py +0 -0
  24. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/__init__.py +0 -0
  25. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/_identity.py +0 -0
  26. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
  27. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/_spec.py +0 -0
  28. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/antigravity_cli.py +0 -0
  29. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/claude.py +0 -0
  30. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/claude_code.py +0 -0
  31. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/claude_code_memory.py +0 -0
  32. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/codex.py +0 -0
  33. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/codex_memory.py +0 -0
  34. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/copilot_cli.py +0 -0
  35. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/cursor.py +0 -0
  36. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/grok.py +0 -0
  37. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/openai.py +0 -0
  38. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/adapters/opencode.py +0 -0
  39. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/capture_log.py +0 -0
  40. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/events.py +0 -0
  41. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/hooks/__init__.py +0 -0
  42. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/hooks/cli.py +0 -0
  43. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/hooks/runner.py +0 -0
  44. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/hooks/runtime_plugin.py +0 -0
  45. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/link.py +0 -0
  46. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/mcp_server.py +0 -0
  47. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/memory_backend.py +0 -0
  48. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/protocols.py +0 -0
  49. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/setup/__init__.py +0 -0
  50. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/setup/agents.py +0 -0
  51. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/setup/memgraph.py +0 -0
  52. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/setup/uninstall.py +0 -0
  53. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/src/memgraph_context_graph/tools.py +0 -0
  54. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/__init__.py +0 -0
  55. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/conftest.py +0 -0
  56. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/fake_agent_cli.py +0 -0
  57. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_antigravity_cli_adapter.py +0 -0
  58. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_capture_log.py +0 -0
  59. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_claude_code_adapter.py +0 -0
  60. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_codex_adapter.py +0 -0
  61. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_config_cli.py +0 -0
  62. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_context_before_tool.py +0 -0
  63. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_copilot_cli_adapter.py +0 -0
  64. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_cursor_adapter.py +0 -0
  65. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_events.py +0 -0
  66. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_grok_adapter.py +0 -0
  67. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_hook_cli.py +0 -0
  68. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_identity.py +0 -0
  69. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_link.py +0 -0
  70. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_opencode_adapter.py +0 -0
  71. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_plugin_hooks.py +0 -0
  72. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_runner_sessions_graph_connector.py +0 -0
  73. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_runtime_plugin.py +0 -0
  74. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_setup_docker.py +0 -0
  75. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/tests/test_spaces.py +0 -0
  76. {memgraph_context_graph-0.7.2 → memgraph_context_graph-0.7.3}/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.2
3
+ Version: 0.7.3
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.2"
3
+ version = "0.7.3"
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" }
@@ -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 in the same tool environment.\n"
261
- "Add it to the `uv tool install memgraph-context-graph ...` command your plugin's bootstrap runs:\n"
262
- " --with context-graph-viewer",
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
 
@@ -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. Use your agents as usual; `memgraph-context-graph ui` shows what they capture.")
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
- summary = agents_mod.wire_plugin(agent, log)
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
- self.line(self.style.muted(subtitle))
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", "Memory for your coding agents. Nothing changes until you confirm.")
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="how the graph knows you")
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="Browse what your agents capture, in this terminal: memgraph-context-graph ui.",
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 own memory is switched off and copied into the graph; they remember from there.",
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("Working… (the first Memgraph start downloads about 1 GB)"))
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
- prompts.outro(
541
- style.success("Context Graph is set up.")
542
- + " Use your agents as usual; `memgraph-context-graph ui` shows what they capture."
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 "find one on localhost:7687, or start one in Docker"
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.0"
18
+ BASE = "memgraph-context-graph[mcp]>=0.7.2"
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 install(*, viewer: bool, log: Callable[[str], None]) -> None:
63
- """Install the tool with every connector (and the viewer), keeping what is already there.
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", BASE]
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 = ""
@@ -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
- "uv tool install 'memgraph-context-graph[mcp]' --with context-graph-spaces"
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"
@@ -440,6 +440,16 @@ def test_the_uv_tool_is_recognised_even_when_the_shell_forces_colour(monkeypatch
440
440
  assert tool.uv_tool_environment()
441
441
 
442
442
 
443
+ def test_grok_is_reported_with_the_trust_it_still_needs(fake_agents, setup_home, project):
444
+ env = engine.look_around(cwd=project)
445
+ choices = engine.default_choices(env, cwd=project)
446
+
447
+ grok = engine._agent(BY_RUNTIME["grok"], choices, lambda _: None)
448
+
449
+ assert grok.status == "ok"
450
+ assert "/hooks-trust" in grok.summary
451
+
452
+
443
453
  def test_codex_is_reported_with_the_trust_it_still_needs_and_claude_code_is_not(fake_agents, setup_home, project):
444
454
  env = engine.look_around(cwd=project)
445
455
  choices = engine.default_choices(env, cwd=project)
@@ -450,3 +460,105 @@ def test_codex_is_reported_with_the_trust_it_still_needs_and_claude_code_is_not(
450
460
  assert codex.status == "ok"
451
461
  assert "records nothing until you trust its hooks" in codex.summary
452
462
  assert "trust" not in claude.summary
463
+
464
+
465
+ def test_reinstalling_the_tool_keeps_the_spaces_package_once_it_is_there(monkeypatch):
466
+ from memgraph_context_graph.setup import tool
467
+
468
+ monkeypatch.setattr(tool, "installed", lambda package: package == "context-graph-spaces")
469
+
470
+ argv = tool.requirements()
471
+
472
+ assert tool.BASE in argv
473
+ assert all(requirement in argv for requirement in tool.CONNECTORS.values())
474
+ assert tool.SPACES[1] in argv
475
+ assert tool.VIEWER[1] not in argv
476
+
477
+ monkeypatch.setattr(tool, "installed", lambda package: False)
478
+ assert tool.SPACES[1] not in tool.requirements()
479
+ assert tool.SPACES[1] in tool.requirements(spaces=True)
480
+
481
+
482
+ def test_the_enterprise_hint_names_the_whole_install_not_just_the_spaces_package(monkeypatch):
483
+ import pytest
484
+
485
+ from memgraph_context_graph import spaces
486
+ from memgraph_context_graph.adapters._identity import HookConfig
487
+
488
+ monkeypatch.setattr(spaces, "_edition_factory", lambda edition: None)
489
+ spaces._reset_cache()
490
+
491
+ with pytest.raises(spaces.SpaceError) as excinfo:
492
+ spaces.spaces_for(HookConfig(edition="enterprise"))
493
+
494
+ hint = str(excinfo.value)
495
+ assert "context-graph-spaces" in hint
496
+ assert "skills-graph" in hint and "sessions-graph" in hint
497
+ spaces._reset_cache()
498
+
499
+
500
+ def test_join_installs_the_spaces_package_inside_the_uv_tool_and_not_elsewhere(monkeypatch, setup_home, capsys):
501
+ from memgraph_context_graph import spaces
502
+ from memgraph_context_graph.setup import tool
503
+
504
+ installs = []
505
+ monkeypatch.setattr(spaces, "_edition_factory", lambda edition: None)
506
+ monkeypatch.setattr(tool, "install", lambda **kwargs: installs.append(kwargs))
507
+
508
+ monkeypatch.setattr(tool, "uv_tool_environment", lambda: False)
509
+ spaces._reset_cache()
510
+ assert main(["join", "bolt://localhost:1", "ante", "--password=x"]) == 1
511
+ assert installs == []
512
+ assert "uv tool install" in capsys.readouterr().err
513
+
514
+ monkeypatch.setattr(tool, "uv_tool_environment", lambda: True)
515
+ spaces._reset_cache()
516
+ main(["join", "bolt://localhost:1", "ante", "--password=x"])
517
+ assert [kwargs["spaces"] for kwargs in installs] == [True]
518
+ spaces._reset_cache()
519
+
520
+
521
+ def test_the_welcome_says_what_the_user_is_getting_before_any_question(fake_agents, setup_home, project):
522
+ from memgraph_context_graph.setup.prompts import ENTER, Prompts, ask
523
+
524
+ env = engine.look_around(cwd=project)
525
+ term = ScriptedTerminal(keys=[ENTER] * 5, columns=100)
526
+
527
+ ask(Prompts(term), engine.default_choices(env, cwd=project), env, preselect=False)
528
+
529
+ first_question = term.text.index("Your name")
530
+ for line in ("about to get a memory", "No account or cloud needed", "Nothing changes until you confirm"):
531
+ assert term.text.index(line) < first_question
532
+
533
+
534
+ def test_the_last_screen_says_what_to_expect_and_only_what_applies():
535
+ choices = engine.Choices(
536
+ runtimes=["claude-code", "codex", "grok"], project_dirs=[], user_id="carol", memory_backend="context-graph"
537
+ )
538
+ env = engine.Environment(edition="community", configured_url=None, configured_user=None, found_agents=())
539
+
540
+ text = "\n".join(engine.next_steps(choices, env))
541
+
542
+ assert "Claude Code and Codex remember through the graph now." in text
543
+ assert "memgraph-context-graph ui" in text
544
+ assert "in Codex, open it and accept its hooks" in text
545
+ assert "in Grok, run /hooks-trust" in text
546
+ assert "memgraph-context-graph uninstall" in text
547
+
548
+ plain = engine.Choices(runtimes=["opencode"], project_dirs=[], user_id="carol", viewer=False)
549
+ quiet = "\n".join(engine.next_steps(plain, env))
550
+
551
+ assert "remember through the graph" not in quiet
552
+ assert "memgraph-context-graph ui" not in quiet
553
+ assert "accept the hooks" not in quiet
554
+ assert "what did we do last time" in quiet
555
+
556
+
557
+ def test_every_line_of_the_welcome_and_the_last_screen_fits_a_narrow_terminal():
558
+ choices = engine.Choices(
559
+ runtimes=["claude-code", "codex", "grok"], project_dirs=[], user_id="carol", memory_backend="context-graph"
560
+ )
561
+ env = engine.Environment(edition="community", configured_url=None, configured_user=None, found_agents=())
562
+
563
+ for line in (*engine.WELCOME, *engine.next_steps(choices, env)):
564
+ assert len(line) <= 78, line