memgraph-context-graph 0.7.0__tar.gz → 0.7.2__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.0 → memgraph_context_graph-0.7.2}/CONTEXT.md +2 -2
  2. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/PKG-INFO +6 -59
  3. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/README.md +5 -58
  4. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/docs/command-hooks.md +1 -1
  5. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/pyproject.toml +1 -1
  6. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/__init__.py +1 -2
  7. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/_identity.py +76 -179
  8. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/_spec.py +48 -9
  9. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/claude.py +5 -23
  10. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/claude_code.py +11 -44
  11. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/codex.py +5 -4
  12. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/copilot_cli.py +2 -1
  13. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/cursor.py +5 -5
  14. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/grok.py +13 -41
  15. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/openai.py +3 -6
  16. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/opencode.py +3 -6
  17. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/cli.py +43 -67
  18. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/events.py +1 -6
  19. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/hooks/cli.py +5 -9
  20. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/hooks/runner.py +23 -29
  21. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/hooks/runtime_plugin.py +6 -13
  22. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/memory_backend.py +1 -0
  23. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/setup/cli.py +3 -5
  24. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/setup/engine.py +7 -3
  25. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/setup/memgraph.py +15 -0
  26. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/setup/prompts.py +27 -21
  27. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/setup/tool.py +5 -4
  28. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/setup/uninstall.py +2 -2
  29. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/tools.py +4 -3
  30. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_codex_adapter.py +19 -0
  31. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_setup.py +47 -5
  32. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_setup_docker.py +17 -1
  33. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/.gitignore +0 -0
  34. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/LICENSE +0 -0
  35. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
  36. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
  37. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/docs/adr/0003-config-path-override.md +0 -0
  38. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
  39. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
  40. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
  41. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/docs/research/2026-10-codex-memory.md +0 -0
  42. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/__init__.py +0 -0
  43. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
  44. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/antigravity_cli.py +0 -0
  45. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/claude_code_memory.py +0 -0
  46. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/adapters/codex_memory.py +0 -0
  47. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/capture_log.py +0 -0
  48. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/hooks/__init__.py +0 -0
  49. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/link.py +0 -0
  50. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/mcp_server.py +0 -0
  51. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/protocols.py +0 -0
  52. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/setup/__init__.py +0 -0
  53. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/setup/agents.py +0 -0
  54. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/src/memgraph_context_graph/spaces.py +0 -0
  55. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/__init__.py +0 -0
  56. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/conftest.py +0 -0
  57. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/fake_agent_cli.py +0 -0
  58. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_antigravity_cli_adapter.py +0 -0
  59. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_capture_log.py +0 -0
  60. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_claude_code_adapter.py +0 -0
  61. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_config_cli.py +0 -0
  62. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_context_before_tool.py +0 -0
  63. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_copilot_cli_adapter.py +0 -0
  64. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_cursor_adapter.py +0 -0
  65. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_dev_scripts.py +0 -0
  66. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_events.py +0 -0
  67. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_grok_adapter.py +0 -0
  68. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_hook_cli.py +0 -0
  69. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_identity.py +0 -0
  70. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_link.py +0 -0
  71. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_opencode_adapter.py +0 -0
  72. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_plugin_hooks.py +0 -0
  73. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_runner_sessions_graph_connector.py +0 -0
  74. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_runtime_plugin.py +0 -0
  75. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_spaces.py +0 -0
  76. {memgraph_context_graph-0.7.0 → memgraph_context_graph-0.7.2}/tests/test_tools.py +0 -0
@@ -85,7 +85,7 @@ _Avoid_: MCP tool (the protocol it is served over, not the thing), Graph Connect
85
85
  - **Claude Code Plugin** installs Claude Code hook wiring invoking the Claude Code runtime adapter command.
86
86
  - **Event Protocol** carries agent activity, not graph semantics.
87
87
  - **Agent Session** may be persisted as a session node by a graph component; **Event Protocol** only carries the session identifier.
88
- - Tool/message events carry `agent_name` when runtime identifies a subagent. Memgraph Context Graph only transports this id; Graph Connectors decide how to use it. Codex adapter doesn't yet handle subagent lifecycle/ids ([#275](https://github.com/memgraph/ai-toolkit/issues/275)).
88
+ - Tool/message events carry `agent_name` when runtime identifies a subagent. Memgraph Context Graph only transports this id; Graph Connectors decide how to use it. The Codex adapter emits no subagent lifecycle events.
89
89
  - Runtime Plugin's generated command calls `hook run <name>`. CLI resolves name via Runtime Registration; plugin never names an adapter class directly.
90
90
  - Built-in Runtime Registrations: `codex`, `claude-code`, `copilot-cli`, `cursor`, `opencode`, `antigravity-cli`, and `grok`. Other packages may publish more.
91
91
 
@@ -110,4 +110,4 @@ _Avoid_: MCP tool (the protocol it is served over, not the thing), Graph Connect
110
110
  - "Session" = activity or graph state? Use **Agent Session** for activity, "session node" for persisted state.
111
111
  - "Plugin" can sound like a graph extension. Use **Runtime Plugin** for host-specific distribution; Graph Connectors own graph interpretation.
112
112
  - Entry-point discovery = **Runtime Registration**, not a runtime/CLI plugin ([#269](https://github.com/memgraph/ai-toolkit/issues/269)).
113
- - Code hasn't fully caught up to **Runtime Adapter** terminology: Event Protocol's `source_sdk` field (`events.py`, set by all 4 adapters, read by Actions Graph's connector) still names the rejected term (ADR 0001). Rename to e.g. `source_adapter` tracked, not done.
113
+ - Code hasn't fully caught up to **Runtime Adapter** terminology: Event Protocol's `source_sdk` field (`events.py`, set by every adapter, read by Actions Graph's connector) still names the rejected term (ADR 0001); a rename to e.g. `source_adapter` is not done.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memgraph-context-graph
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Connect agent SDKs to context-graph components (actions-graph, skills-graph, etc.)
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -431,34 +431,11 @@ name is used. See
431
431
  [ADR 0006](docs/adr/0006-memory-tool-writes-explicit-memory.md) for why this is
432
432
  the one write the model makes over MCP.
433
433
 
434
- ### OpenAI Codex Plugin
434
+ ### Codex and Claude Code Plugins
435
435
 
436
- Codex hook configuration can be installed as a user-level Codex plugin.
437
-
438
- The runtime-plugin flow is:
439
-
440
- ```text
441
- Codex Plugin -> Codex Runtime Adapter -> Event Protocol -> Graph Connector -> Memgraph
442
- ```
443
-
444
- The plugin installs Codex hook wiring. The Codex runtime adapter normalizes the hook payload. Graph connectors such as `SkillGraphConnector` decide what those events mean in their graph.
445
-
446
- Plugin source:
447
-
448
- ```text
449
- context-graph/plugins/memgraph-context-graph-codex
450
- ```
451
-
452
- Register the public Git-backed marketplace and install the plugin:
453
-
454
- ```bash
455
- codex plugin marketplace add memgraph/ai-toolkit --sparse .agents/plugins
456
- codex plugin add context-graph@context-graph-plugins
457
- ```
458
-
459
- Both are non-interactive; `codex plugin add` installs and enables the plugin in one step (`codex plugin list --json` confirms `"enabled": true`).
460
-
461
- Check the installed hook environment with:
436
+ Both runtimes have a distributable Runtime Plugin that wires hooks and the MCP server; marketplace
437
+ install steps are in [plugins/memgraph-context-graph-codex](../plugins/memgraph-context-graph-codex/README.md) and
438
+ [plugins/memgraph-context-graph-claude](../plugins/memgraph-context-graph-claude/README.md). Check an install with:
462
439
 
463
440
  ```bash
464
441
  memgraph-context-graph doctor --runtime codex --connector skills-graph --connector actions-graph --connector sessions-graph
@@ -466,34 +443,6 @@ memgraph-context-graph doctor --runtime codex --connector skills-graph --connect
466
443
 
467
444
  Graph credentials live in `~/.config/context-graph/config.toml` (written by `setup`/`config set`), not in plugin hook files or the process environment — hooks read that file at runtime. See [Configuration](#configuration).
468
445
 
469
- ### Claude Code Plugin
470
-
471
- Claude Code hook configuration can be installed as a Claude Code plugin.
472
-
473
- The runtime-plugin flow is:
474
-
475
- ```text
476
- Claude Code Plugin -> Claude Code Runtime Adapter -> Event Protocol -> Graph Connector -> Memgraph
477
- ```
478
-
479
- For a public Git-backed marketplace install, add the marketplace inside Claude Code:
480
-
481
- ```text
482
- /plugin marketplace add memgraph/ai-toolkit
483
- ```
484
-
485
- Then install:
486
-
487
- ```text
488
- /plugin install context-graph@context-graph-plugins
489
- ```
490
-
491
- Check the installed hook environment with:
492
-
493
- ```bash
494
- memgraph-context-graph doctor --runtime claude-code --connector skills-graph --connector actions-graph --connector sessions-graph
495
- ```
496
-
497
446
  ### Source Development
498
447
 
499
448
  For source development and per-project experiments, you can generate local Codex hook files:
@@ -665,9 +614,7 @@ class MyRuntimePlugin:
665
614
  def build_hooks_config(self, command: str, *, timeout: int = 30) -> dict:
666
615
  return build_hooks_config(command, timeout=timeout)
667
616
 
668
- # init(project_dir, connectors, **kwargs) is optional -- omit it if your
669
- # runtime has no project-local hook-config file to generate (matching
670
- # ClaudeCodeHooksAdapter's own plugin, which doesn't define one yet).
617
+ # init(project_dir, connectors, **kwargs) is optional: omit it when there is no project-local file to write.
671
618
 
672
619
 
673
620
  PLUGIN = MyRuntimePlugin()
@@ -406,34 +406,11 @@ name is used. See
406
406
  [ADR 0006](docs/adr/0006-memory-tool-writes-explicit-memory.md) for why this is
407
407
  the one write the model makes over MCP.
408
408
 
409
- ### OpenAI Codex Plugin
409
+ ### Codex and Claude Code Plugins
410
410
 
411
- Codex hook configuration can be installed as a user-level Codex plugin.
412
-
413
- The runtime-plugin flow is:
414
-
415
- ```text
416
- Codex Plugin -> Codex Runtime Adapter -> Event Protocol -> Graph Connector -> Memgraph
417
- ```
418
-
419
- The plugin installs Codex hook wiring. The Codex runtime adapter normalizes the hook payload. Graph connectors such as `SkillGraphConnector` decide what those events mean in their graph.
420
-
421
- Plugin source:
422
-
423
- ```text
424
- context-graph/plugins/memgraph-context-graph-codex
425
- ```
426
-
427
- Register the public Git-backed marketplace and install the plugin:
428
-
429
- ```bash
430
- codex plugin marketplace add memgraph/ai-toolkit --sparse .agents/plugins
431
- codex plugin add context-graph@context-graph-plugins
432
- ```
433
-
434
- Both are non-interactive; `codex plugin add` installs and enables the plugin in one step (`codex plugin list --json` confirms `"enabled": true`).
435
-
436
- Check the installed hook environment with:
411
+ Both runtimes have a distributable Runtime Plugin that wires hooks and the MCP server; marketplace
412
+ install steps are in [plugins/memgraph-context-graph-codex](../plugins/memgraph-context-graph-codex/README.md) and
413
+ [plugins/memgraph-context-graph-claude](../plugins/memgraph-context-graph-claude/README.md). Check an install with:
437
414
 
438
415
  ```bash
439
416
  memgraph-context-graph doctor --runtime codex --connector skills-graph --connector actions-graph --connector sessions-graph
@@ -441,34 +418,6 @@ memgraph-context-graph doctor --runtime codex --connector skills-graph --connect
441
418
 
442
419
  Graph credentials live in `~/.config/context-graph/config.toml` (written by `setup`/`config set`), not in plugin hook files or the process environment — hooks read that file at runtime. See [Configuration](#configuration).
443
420
 
444
- ### Claude Code Plugin
445
-
446
- Claude Code hook configuration can be installed as a Claude Code plugin.
447
-
448
- The runtime-plugin flow is:
449
-
450
- ```text
451
- Claude Code Plugin -> Claude Code Runtime Adapter -> Event Protocol -> Graph Connector -> Memgraph
452
- ```
453
-
454
- For a public Git-backed marketplace install, add the marketplace inside Claude Code:
455
-
456
- ```text
457
- /plugin marketplace add memgraph/ai-toolkit
458
- ```
459
-
460
- Then install:
461
-
462
- ```text
463
- /plugin install context-graph@context-graph-plugins
464
- ```
465
-
466
- Check the installed hook environment with:
467
-
468
- ```bash
469
- memgraph-context-graph doctor --runtime claude-code --connector skills-graph --connector actions-graph --connector sessions-graph
470
- ```
471
-
472
421
  ### Source Development
473
422
 
474
423
  For source development and per-project experiments, you can generate local Codex hook files:
@@ -640,9 +589,7 @@ class MyRuntimePlugin:
640
589
  def build_hooks_config(self, command: str, *, timeout: int = 30) -> dict:
641
590
  return build_hooks_config(command, timeout=timeout)
642
591
 
643
- # init(project_dir, connectors, **kwargs) is optional -- omit it if your
644
- # runtime has no project-local hook-config file to generate (matching
645
- # ClaudeCodeHooksAdapter's own plugin, which doesn't define one yet).
592
+ # init(project_dir, connectors, **kwargs) is optional: omit it when there is no project-local file to write.
646
593
 
647
594
 
648
595
  PLUGIN = MyRuntimePlugin()
@@ -205,7 +205,7 @@ session end, so adapters emit two different events:
205
205
  | Runtime | Turn end | Session end |
206
206
  |---|---|---|
207
207
  | Claude Code | `Stop` (records `last_assistant_message`) | `SessionEnd` |
208
- | Codex | `Stop` (records `last_assistant_message`) | none |
208
+ | Codex | `Stop` (records `last_assistant_message`) | `SessionEnd`, only when Codex ends the session; with its shared background server `/exit` just detaches |
209
209
  | Copilot CLI | `agentStop` | `sessionEnd` |
210
210
  | Cursor | `stop` | `sessionEnd` |
211
211
  | OpenCode | `session.execution.succeeded` / `.failed` | `session.deleted` |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memgraph-context-graph"
3
- version = "0.7.0"
3
+ version = "0.7.2"
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" }
@@ -45,9 +45,8 @@ from .protocols import GraphConnector, RuntimeAdapter
45
45
  try:
46
46
  __version__ = metadata.version(__package__ or __name__)
47
47
  except metadata.PackageNotFoundError:
48
- # Case where package metadata is not available.
49
48
  __version__ = ""
50
- del metadata # optional, avoids polluting the results of dir(__package__)
49
+ del metadata
51
50
 
52
51
  __all__ = [
53
52
  "AgentEndEvent",
@@ -25,22 +25,12 @@ from __future__ import annotations
25
25
 
26
26
  import os
27
27
  import stat
28
- from dataclasses import dataclass, field
28
+ from dataclasses import dataclass, field, replace
29
29
  from pathlib import Path
30
30
  from typing import Any
31
31
 
32
- #: Points hooks at a different config *file*. Not a way to supply configuration
33
- #: values -- ADR 0002 removed those from the environment deliberately, because
34
- #: hook subprocesses do not source shell profiles and relying on ambient env
35
- #: caused `doctor` and hooks to disagree about what was configured.
36
- #:
37
- #: This is the opposite situation: a parent process that spawns a session hands
38
- #: the path down explicitly to its own child, so nothing is ambient and nothing
39
- #: can drift. It exists because the config file is otherwise a single global,
40
- #: which means pointing ONE session at a different Memgraph silently redirects
41
- #: every other Claude Code session running on the machine -- observed while
42
- #: building the eval gold slice, where an unrelated session's activity landed
43
- #: in the graph under test.
32
+ #: Selects which config *file* hooks read, never a value in it (ADR 0003): a parent
33
+ #: process hands the path to the session it spawns, so nothing is ambient.
44
34
  CONFIG_PATH_ENV = "CONTEXT_GRAPH_CONFIG"
45
35
 
46
36
  _DEFAULT_CONFIG_DIR = Path.home() / ".config" / "context-graph"
@@ -75,13 +65,7 @@ MEMORY_BACKEND_GRAPH = "context-graph"
75
65
  MEMORY_BACKEND_NATIVE = "native"
76
66
  MEMORY_BACKENDS = frozenset({MEMORY_BACKEND_GRAPH, MEMORY_BACKEND_NATIVE})
77
67
 
78
- _RECONCILE_DEFAULTS = {
79
- "auto_reconcile": None,
80
- }
81
-
82
- #: Shared truthy/falsy vocabulary for TOML/CLI boolean flags — kept here so
83
- #: cli.py can validate `config set` input against the same set parse_bool_flag
84
- #: accepts, instead of redefining it inline.
68
+ #: The boolean spellings parse_bool_flag accepts, shared with ``config set`` validation.
85
69
  TRUTHY_VALUES = frozenset({"1", "true", "yes", "on"})
86
70
  FALSY_VALUES = frozenset({"0", "false", "no", "off"})
87
71
 
@@ -101,7 +85,7 @@ class HookConfig:
101
85
  memgraph_database: str = _MEMGRAPH_DEFAULTS["database"]
102
86
  openai_api_key: str = _LLM_DEFAULTS["openai_api_key"]
103
87
  anthropic_api_key: str = _LLM_DEFAULTS["anthropic_api_key"]
104
- auto_reconcile: bool | None = _RECONCILE_DEFAULTS["auto_reconcile"]
88
+ auto_reconcile: bool | None = None
105
89
  #: The model recall embeds with; None means the consumer's own default.
106
90
  embedding_model: str | None = None
107
91
  #: Every other ``[recall]`` key (lanes and widths), as written: the recall
@@ -133,18 +117,11 @@ class HookConfig:
133
117
 
134
118
 
135
119
  def load_config() -> HookConfig:
136
- """Load hook configuration from the config file.
137
-
138
- Returns a HookConfig with defaults for any missing values.
139
- Caches the result for the lifetime of the process.
140
- """
120
+ """Load hook configuration from the config file, with defaults for missing values; cached per process."""
141
121
  global _cached_config
142
- if _cached_config is not None:
143
- return _cached_config
144
-
145
- config = _read_config_file()
146
- _cached_config = config
147
- return config
122
+ if _cached_config is None:
123
+ _cached_config = _read_config_file()
124
+ return _cached_config
148
125
 
149
126
 
150
127
  def reload_config() -> HookConfig:
@@ -190,13 +167,10 @@ def resolve_memgraph_env(
190
167
 
191
168
 
192
169
  def resolve_llm_env() -> dict[str, str]:
193
- """Resolve LLM API key settings for hook subprocesses.
170
+ """Resolve LLM API keys for hook subprocesses, from the config file only.
194
171
 
195
- Config file only — mirrors :func:`resolve_memgraph_env`, but there is no
196
- CLI-flag override path for these since nothing resolves them from argparse.
197
- Values are empty strings when not configured. Hook-spawned children must
198
- overlay these empty values too so ambient credentials cannot supply a key
199
- absent from the selected config file.
172
+ An unconfigured key is an empty string. Hook-spawned children must overlay these
173
+ empty values too, so ambient credentials cannot supply a key the config file lacks.
200
174
  """
201
175
  config = load_config()
202
176
  return {
@@ -208,42 +182,25 @@ def resolve_llm_env() -> dict[str, str]:
208
182
  def resolve_auto_reconcile() -> bool:
209
183
  """Resolve whether sessions-graph should auto-trigger reconciliation on SESSION_END.
210
184
 
211
- Config file only — mirrors :func:`resolve_llm_env`. The config-file value
212
- is ``None`` when never configured; per ADR 0002 (config-file-only-hook-
213
- resolution) there is no ambient env var fallback to preserve here, so
214
- "never configured" collapses to ``False``, matching
215
- ``SessionsGraphConnector``'s own default. Off by default given LightRAG
216
- entity extraction's LLM cost; opt in with
217
- ``memgraph-context-graph config set reconcile.auto_reconcile true``.
185
+ Config file only (ADR 0002); never configured means ``False``, matching
186
+ ``SessionsGraphConnector``'s default, since reconciliation costs LLM calls.
218
187
  """
219
188
  return bool(load_config().auto_reconcile)
220
189
 
221
190
 
222
191
  def resolve_embedding_model() -> str | None:
223
- """The configured ``[recall] embedding_model``, or None when unset.
224
-
225
- Config file only, like :func:`resolve_auto_reconcile`. None leaves the
226
- choice to sessions-graph's own default rather than duplicating it here.
227
- """
192
+ """The configured ``[recall] embedding_model``, or None to leave sessions-graph's default."""
228
193
  return load_config().embedding_model
229
194
 
230
195
 
231
196
  def resolve_ontology() -> tuple[str | None, str | None]:
232
- """The configured ``[ontology]`` (path, derive), each None when unset.
233
-
234
- Config file only, like :func:`resolve_auto_reconcile`. sessions-graph owns
235
- what the values mean and their defaults.
236
- """
197
+ """The configured ``[ontology]`` (path, derive), each None when unset; sessions-graph owns their meaning."""
237
198
  config = load_config()
238
199
  return config.ontology_path, config.ontology_derive
239
200
 
240
201
 
241
202
  def parse_bool_flag(value: str | bool) -> bool:
242
- """Parse a TOML/CLI boolean flag string. Truthy: "1"/"true"/"yes"/"on" (case-insensitive).
243
-
244
- Passing an actual ``bool`` through unchanged guards against a future
245
- swap to a real TOML parser (which would hand back native booleans).
246
- """
203
+ """Parse a TOML/CLI boolean flag: :data:`TRUTHY_VALUES` (case-insensitive) is true, a ``bool`` passes through."""
247
204
  if isinstance(value, bool):
248
205
  return value
249
206
  return value.strip().lower() in TRUTHY_VALUES
@@ -271,52 +228,24 @@ def write_config(
271
228
  Only updates supplied values; preserves existing values for unspecified keys.
272
229
  Creates the file with 0600 permissions if it does not exist.
273
230
  """
274
- global _cached_config
275
-
276
- # Read existing config as base.
277
- existing = _read_config_file()
278
-
279
- final_edition = edition if edition is not None else existing.edition
280
- final_user_id = user_id if user_id is not None else existing.user_id
281
- final_url = memgraph_url if memgraph_url is not None else existing.memgraph_url
282
- final_user = memgraph_user if memgraph_user is not None else existing.memgraph_user
283
- final_password = memgraph_password if memgraph_password is not None else existing.memgraph_password
284
- final_database = memgraph_database if memgraph_database is not None else existing.memgraph_database
285
- final_openai_api_key = openai_api_key if openai_api_key is not None else existing.openai_api_key
286
- final_anthropic_api_key = anthropic_api_key if anthropic_api_key is not None else existing.anthropic_api_key
287
- final_auto_reconcile = auto_reconcile if auto_reconcile is not None else existing.auto_reconcile
288
- final_embedding_model = embedding_model if embedding_model is not None else existing.embedding_model
289
- final_ontology_path = ontology_path if ontology_path is not None else existing.ontology_path
290
- final_ontology_derive = ontology_derive if ontology_derive is not None else existing.ontology_derive
291
- final_github_token = github_token if github_token is not None else existing.github_token
292
- final_memory_backend = memory_backend if memory_backend is not None else existing.memory_backend
293
-
294
- content = _render_config(
295
- edition=final_edition,
296
- user_id=final_user_id or "",
297
- url=final_url,
298
- user=final_user,
299
- password=final_password,
300
- database=final_database,
301
- openai_api_key=final_openai_api_key,
302
- anthropic_api_key=final_anthropic_api_key,
303
- auto_reconcile=final_auto_reconcile,
304
- embedding_model=final_embedding_model,
305
- recall_settings=existing.recall_settings,
306
- ontology_path=final_ontology_path,
307
- ontology_derive=final_ontology_derive,
308
- github_token=final_github_token,
309
- memory_backend=final_memory_backend,
310
- )
311
-
312
- path = config_file()
313
- path.parent.mkdir(parents=True, exist_ok=True)
314
- path.write_text(content, encoding="utf-8")
315
- path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600
316
-
317
- # Invalidate cache.
318
- _cached_config = None
319
- return config_file()
231
+ updates = {
232
+ "edition": edition,
233
+ "user_id": user_id,
234
+ "memgraph_url": memgraph_url,
235
+ "memgraph_user": memgraph_user,
236
+ "memgraph_password": memgraph_password,
237
+ "memgraph_database": memgraph_database,
238
+ "openai_api_key": openai_api_key,
239
+ "anthropic_api_key": anthropic_api_key,
240
+ "auto_reconcile": auto_reconcile,
241
+ "embedding_model": embedding_model,
242
+ "ontology_path": ontology_path,
243
+ "ontology_derive": ontology_derive,
244
+ "github_token": github_token,
245
+ "memory_backend": memory_backend,
246
+ }
247
+ config = replace(_read_config_file(), **{key: value for key, value in updates.items() if value is not None})
248
+ return _write_config_file(_render_config(config))
320
249
 
321
250
 
322
251
  def write_full_config(
@@ -328,7 +257,7 @@ def write_full_config(
328
257
  memgraph_database: str = _MEMGRAPH_DEFAULTS["database"],
329
258
  openai_api_key: str = _LLM_DEFAULTS["openai_api_key"],
330
259
  anthropic_api_key: str = _LLM_DEFAULTS["anthropic_api_key"],
331
- auto_reconcile: bool | None = _RECONCILE_DEFAULTS["auto_reconcile"],
260
+ auto_reconcile: bool | None = None,
332
261
  ) -> Path:
333
262
  """Write a complete config file, resetting identity, Memgraph and LLM settings to what is given.
334
263
 
@@ -336,49 +265,29 @@ def write_full_config(
336
265
  edition are kept from the existing file, since only ``config set`` or a hand edit ever sets them.
337
266
  ``auto_reconcile`` is kept likewise unless given here. :func:`write_config` is the partial update.
338
267
  """
339
- global _cached_config
340
-
341
268
  existing = _read_config_file()
342
- final_auto_reconcile = auto_reconcile if auto_reconcile is not None else existing.auto_reconcile
343
-
344
- content = _render_config(
345
- edition=existing.edition,
269
+ config = replace(
270
+ existing,
346
271
  user_id=user_id,
347
- url=memgraph_url,
348
- user=memgraph_user,
349
- password=memgraph_password,
350
- database=memgraph_database,
272
+ memgraph_url=memgraph_url,
273
+ memgraph_user=memgraph_user,
274
+ memgraph_password=memgraph_password,
275
+ memgraph_database=memgraph_database,
351
276
  openai_api_key=openai_api_key,
352
277
  anthropic_api_key=anthropic_api_key,
353
- auto_reconcile=final_auto_reconcile,
354
- embedding_model=existing.embedding_model,
355
- recall_settings=existing.recall_settings,
356
- ontology_path=existing.ontology_path,
357
- ontology_derive=existing.ontology_derive,
358
- github_token=existing.github_token,
359
- memory_backend=existing.memory_backend,
278
+ auto_reconcile=auto_reconcile if auto_reconcile is not None else existing.auto_reconcile,
360
279
  )
280
+ return _write_config_file(_render_config(config))
281
+
361
282
 
283
+ def _write_config_file(content: str) -> Path:
284
+ global _cached_config
362
285
  path = config_file()
363
286
  path.parent.mkdir(parents=True, exist_ok=True)
364
287
  path.write_text(content, encoding="utf-8")
365
- path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600
366
-
288
+ path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600: the file holds credentials.
367
289
  _cached_config = None
368
- return config_file()
369
-
370
-
371
- def config_file_path() -> Path:
372
- """Return the config file path (for display purposes)."""
373
- return config_file()
374
-
375
-
376
- def config_dir_path() -> Path:
377
- """Return the directory holding the config file actually in use."""
378
- return config_file().parent
379
-
380
-
381
- # --- Internal helpers ---
290
+ return path
382
291
 
383
292
 
384
293
  def _read_config_file() -> HookConfig:
@@ -466,62 +375,50 @@ def _parse_toml(path: Path, problems: list[str] | None = None) -> dict[str, dict
466
375
  return sections
467
376
 
468
377
 
469
- def _render_config(
470
- *,
471
- edition: str | None = None,
472
- user_id: str,
473
- url: str,
474
- user: str,
475
- password: str,
476
- database: str,
477
- openai_api_key: str,
478
- anthropic_api_key: str,
479
- auto_reconcile: bool | None,
480
- embedding_model: str | None = None,
481
- recall_settings: dict[str, str] | None = None,
482
- ontology_path: str | None = None,
483
- ontology_derive: str | None = None,
484
- github_token: str | None = None,
485
- memory_backend: str | None = None,
486
- ) -> str:
378
+ def _render_config(config: HookConfig) -> str:
487
379
  """Render the full config file content.
488
380
 
489
- The ``[reconcile]`` section is omitted entirely when ``auto_reconcile`` is
490
- ``None`` (never configured), so a fresh read of the file resolves it back
491
- to ``None`` rather than a concrete ``false`` — see
492
- :func:`resolve_auto_reconcile` for why that distinction matters.
493
- ``[recall]``, ``[ontology]``, ``[github]`` and ``[memory]`` are likewise omitted while they hold nothing.
381
+ ``[reconcile]`` is omitted while ``auto_reconcile`` is ``None`` so a re-read keeps "never configured"
382
+ distinct from an explicit ``false``; ``[recall]``, ``[ontology]``, ``[github]`` and ``[memory]`` are
383
+ omitted while they hold nothing.
494
384
  """
385
+ user_id = config.user_id or ""
495
386
  lines = [
496
387
  "# Context Graph hook configuration",
497
388
  "# Generated by: memgraph-context-graph config set / bootstrap",
498
389
  f"# Location: {config_file()}",
499
390
  "",
500
- *([f'edition = "{edition}"', ""] if edition else []),
391
+ *([f'edition = "{config.edition}"', ""] if config.edition else []),
501
392
  # Enterprise takes the user and their database from the Memgraph login, so it writes neither.
502
- *(["[identity]", f'user_id = "{user_id}"', ""] if edition != "enterprise" else []),
393
+ *(["[identity]", f'user_id = "{user_id}"', ""] if config.edition != "enterprise" else []),
503
394
  "[memgraph]",
504
- f'url = "{url}"',
505
- f'user = "{user}"',
506
- f'password = "{password}"',
507
- *([f'database = "{database}"'] if edition != "enterprise" else []),
395
+ f'url = "{config.memgraph_url}"',
396
+ f'user = "{config.memgraph_user}"',
397
+ f'password = "{config.memgraph_password}"',
398
+ *([f'database = "{config.memgraph_database}"'] if config.edition != "enterprise" else []),
508
399
  "",
509
400
  "[llm]",
510
- f'openai_api_key = "{openai_api_key}"',
511
- f'anthropic_api_key = "{anthropic_api_key}"',
401
+ f'openai_api_key = "{config.openai_api_key}"',
402
+ f'anthropic_api_key = "{config.anthropic_api_key}"',
512
403
  ]
513
- if auto_reconcile is not None:
514
- lines += ["", "[reconcile]", f"auto_reconcile = {'true' if auto_reconcile else 'false'}"]
515
- recall = {"embedding_model": embedding_model, **(recall_settings or {})} if embedding_model else recall_settings
404
+ if config.auto_reconcile is not None:
405
+ lines += ["", "[reconcile]", f"auto_reconcile = {'true' if config.auto_reconcile else 'false'}"]
406
+ recall = (
407
+ {"embedding_model": config.embedding_model, **config.recall_settings}
408
+ if config.embedding_model
409
+ else config.recall_settings
410
+ )
516
411
  if recall:
517
412
  lines += ["", "[recall]", *(f'{key} = "{value}"' for key, value in recall.items())]
518
- ontology = {key: value for key, value in (("path", ontology_path), ("derive", ontology_derive)) if value}
413
+ ontology = {
414
+ key: value for key, value in (("path", config.ontology_path), ("derive", config.ontology_derive)) if value
415
+ }
519
416
  if ontology:
520
417
  lines += ["", "[ontology]", *(f'{key} = "{value}"' for key, value in ontology.items())]
521
- if github_token:
522
- lines += ["", "[github]", f'token = "{github_token}"']
523
- if memory_backend:
524
- lines += ["", "[memory]", f'backend = "{memory_backend}"']
418
+ if config.github_token:
419
+ lines += ["", "[github]", f'token = "{config.github_token}"']
420
+ if config.memory_backend:
421
+ lines += ["", "[memory]", f'backend = "{config.memory_backend}"']
525
422
  lines.append("")
526
423
  return "\n".join(lines)
527
424