memgraph-context-graph 0.5.0__tar.gz → 0.6.0__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 (77) hide show
  1. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/PKG-INFO +31 -43
  2. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/README.md +29 -42
  3. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/docs/command-hooks.md +3 -6
  4. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/pyproject.toml +9 -1
  5. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/_identity.py +43 -31
  6. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/_spec.py +73 -2
  7. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/opencode.py +2 -2
  8. memgraph_context_graph-0.6.0/src/memgraph_context_graph/capture_log.py +417 -0
  9. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/cli.py +150 -211
  10. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/hooks/cli.py +19 -30
  11. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/hooks/runner.py +215 -45
  12. memgraph_context_graph-0.6.0/src/memgraph_context_graph/link.py +79 -0
  13. memgraph_context_graph-0.6.0/src/memgraph_context_graph/setup/__init__.py +1 -0
  14. memgraph_context_graph-0.6.0/src/memgraph_context_graph/setup/agents.py +249 -0
  15. memgraph_context_graph-0.6.0/src/memgraph_context_graph/setup/cli.py +143 -0
  16. memgraph_context_graph-0.6.0/src/memgraph_context_graph/setup/engine.py +306 -0
  17. memgraph_context_graph-0.6.0/src/memgraph_context_graph/setup/memgraph.py +294 -0
  18. memgraph_context_graph-0.6.0/src/memgraph_context_graph/setup/tool.py +88 -0
  19. memgraph_context_graph-0.6.0/src/memgraph_context_graph/setup/tui.py +227 -0
  20. memgraph_context_graph-0.6.0/src/memgraph_context_graph/setup/uninstall.py +110 -0
  21. memgraph_context_graph-0.6.0/tests/conftest.py +71 -0
  22. memgraph_context_graph-0.6.0/tests/fake_agent_cli.py +61 -0
  23. memgraph_context_graph-0.6.0/tests/test_capture_log.py +289 -0
  24. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_dev_scripts.py +23 -19
  25. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_hook_cli.py +0 -202
  26. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_plugin_hooks.py +2 -2
  27. memgraph_context_graph-0.6.0/tests/test_setup.py +267 -0
  28. memgraph_context_graph-0.6.0/tests/test_setup_docker.py +162 -0
  29. memgraph_context_graph-0.5.0/src/memgraph_context_graph/link.py +0 -50
  30. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/.gitignore +0 -0
  31. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/CONTEXT.md +0 -0
  32. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/LICENSE +0 -0
  33. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
  34. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
  35. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/docs/adr/0003-config-path-override.md +0 -0
  36. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
  37. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
  38. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
  39. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/docs/research/2026-10-codex-memory.md +0 -0
  40. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/__init__.py +0 -0
  41. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/__init__.py +0 -0
  42. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
  43. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/antigravity_cli.py +0 -0
  44. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/claude.py +0 -0
  45. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/claude_code.py +0 -0
  46. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/claude_code_memory.py +0 -0
  47. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/codex.py +0 -0
  48. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/codex_memory.py +0 -0
  49. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/copilot_cli.py +0 -0
  50. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/cursor.py +0 -0
  51. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/grok.py +0 -0
  52. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/adapters/openai.py +0 -0
  53. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/events.py +0 -0
  54. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/hooks/__init__.py +0 -0
  55. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/hooks/runtime_plugin.py +0 -0
  56. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/mcp_server.py +0 -0
  57. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/memory_backend.py +0 -0
  58. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/protocols.py +0 -0
  59. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/spaces.py +0 -0
  60. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/src/memgraph_context_graph/tools.py +0 -0
  61. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/__init__.py +0 -0
  62. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_antigravity_cli_adapter.py +0 -0
  63. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_claude_code_adapter.py +0 -0
  64. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_codex_adapter.py +0 -0
  65. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_config_cli.py +0 -0
  66. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_context_before_tool.py +0 -0
  67. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_copilot_cli_adapter.py +0 -0
  68. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_cursor_adapter.py +0 -0
  69. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_events.py +0 -0
  70. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_grok_adapter.py +0 -0
  71. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_identity.py +0 -0
  72. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_link.py +0 -0
  73. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_opencode_adapter.py +0 -0
  74. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_runner_sessions_graph_connector.py +0 -0
  75. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_runtime_plugin.py +0 -0
  76. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/tests/test_spaces.py +0 -0
  77. {memgraph_context_graph-0.5.0 → memgraph_context_graph-0.6.0}/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.5.0
3
+ Version: 0.6.0
4
4
  Summary: Connect agent SDKs to context-graph components (actions-graph, skills-graph, etc.)
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Programming Language :: Python :: 3.13
11
11
  Requires-Python: >=3.10
12
12
  Requires-Dist: memgraph-toolbox>=0.1.11
13
+ Requires-Dist: textual<9,>=8.2
13
14
  Requires-Dist: tomlkit<1,>=0.13
14
15
  Provides-Extra: claude
15
16
  Requires-Dist: claude-agent-sdk>=0.1.0; extra == 'claude'
@@ -49,7 +50,7 @@ uv tool install "memgraph-context-graph[mcp]" --with "skills-graph[memgraph-cont
49
50
 
50
51
  The `mcp` extra is what `memgraph-context-graph mcp` serves [recall](#recall-memory-for-the-harnesss-model) with.
51
52
 
52
- Or use the plugin bootstrap scripts; they fall back to `uvx` if the tool is not installed yet.
53
+ Or run `memgraph-context-graph setup`, which installs whatever connectors are missing (see [Setup](#setup)).
53
54
 
54
55
  For SDK usage inside an application:
55
56
 
@@ -178,7 +179,8 @@ Implemented:
178
179
  | Grok Build | `GrokHooksAdapter` | Command receives one JSON object on `stdin` |
179
180
 
180
181
  Codex and Claude Code have distributable Runtime Plugins. Copilot CLI, Cursor,
181
- OpenCode, Antigravity CLI, and Grok Build use project-local wiring generated by `hook init`:
182
+ OpenCode, Antigravity CLI, and Grok Build use project-local wiring, which `setup`
183
+ writes (or, one runtime at a time, `hook init`):
182
184
 
183
185
  ```bash
184
186
  memgraph-context-graph hook init copilot-cli
@@ -191,44 +193,32 @@ memgraph-context-graph hook init grok
191
193
  See the [command-hook reference](docs/command-hooks.md) for generated paths,
192
194
  payload differences, and current upstream hook documentation.
193
195
 
194
- ### First-Time Plugin Setup
196
+ ### Setup
195
197
 
196
- For Codex and Claude Code plugins, the recommended first-run path is the bootstrap command. It installs the runtime package (with the connector extras), checks Memgraph, and runs `doctor`.
197
-
198
- Prerequisites:
199
-
200
- - `uv` on `PATH`. (`uv` manages Python for the tool; if uv-managed Python downloads are blocked, install Python 3.10+ and rerun bootstrap.)
201
- - Memgraph running and reachable over Bolt. Defaults are `bolt://localhost:7687`, empty user/password, database `memgraph`. If it isn't running locally:
202
-
203
- ```bash
204
- docker run --rm -p 7687:7687 memgraph/memgraph-mage
205
- ```
206
-
207
- **1. Bootstrap all three connectors** (this is what the installed plugin wires into its hooks):
198
+ `memgraph-context-graph setup` installs Context Graph for every agent on the machine; the
199
+ [family README](../README.md#getting-started) walks through it. In short:
208
200
 
209
201
  ```bash
210
- # Codex
211
- memgraph-context-graph bootstrap --runtime codex \
212
- --connector skills-graph --connector actions-graph --connector sessions-graph
213
-
214
- # Claude Code
215
- memgraph-context-graph bootstrap --runtime claude-code \
216
- --connector skills-graph --connector actions-graph --connector sessions-graph
202
+ memgraph-context-graph setup # interactive on a terminal; plain otherwise
203
+ memgraph-context-graph setup claude-code codex # just these agents
204
+ memgraph-context-graph setup cursor --project-dir ~/work/billing
205
+ memgraph-context-graph setup --plan # print the plan, change nothing
206
+ memgraph-context-graph uninstall [runtime ...] # remove the wiring; the graph and config stay
217
207
  ```
218
208
 
219
- The plugin wrapper script runs the same command (and falls back to `uvx` if the tool isn't installed yet):
220
-
221
- ```bash
222
- ./scripts/bootstrap.sh
223
- ```
209
+ It finds or starts Memgraph (a `memgraph-mage` container with a volume, on a free port when 7687 is taken),
210
+ writes `memgraph.url` and `identity.user_id`, installs missing connectors and the viewer into the uv tool,
211
+ wires each agent, and runs `doctor`'s checks. Every step checks before it acts, so rerunning only does what's
212
+ missing. The TUI starts only when stdin and stdout are both terminals; the plugins' `scripts/bootstrap.sh`
213
+ runs `setup <runtime> --no-tui`.
224
214
 
225
- **2. Configure identity and connection.** Bootstrap writes `~/.config/context-graph/config.toml`; hooks read their configuration from that file at runtime (see [Configuration](#configuration) — env vars are **not** read at hook time). Set your identity, which sessions-graph requires:
215
+ **Identity and connection.** setup writes `~/.config/context-graph/config.toml`; hooks read their configuration from that file at runtime (see [Configuration](#configuration) — env vars are **not** read at hook time). To change your identity later:
226
216
 
227
217
  ```bash
228
218
  memgraph-context-graph config set identity.user_id "your-name"
229
219
  ```
230
220
 
231
- The Memgraph connection defaults to `bolt://localhost:7687`. For a remote or HA instance:
221
+ For a remote or HA instance, pass `setup --memgraph-url`, or set it with credentials afterwards:
232
222
 
233
223
  ```bash
234
224
  memgraph-context-graph config set memgraph.url "neo4j://<coordinator-host>:7687"
@@ -237,7 +227,7 @@ memgraph-context-graph config set memgraph.password # prompts; stored 060
237
227
  memgraph-context-graph config set memgraph.database "memgraph"
238
228
  ```
239
229
 
240
- **3. Verify:**
230
+ **Verify** (setup runs the same checks):
241
231
 
242
232
  ```bash
243
233
  memgraph-context-graph config show
@@ -292,7 +282,7 @@ turns_k = "8"
292
282
  [sessions-graph § reconciliation](../sessions-graph/README.md#session-reconciliation)),
293
283
  which needs no LLM. `[llm]` is optional: a key only lets reconciliation learn
294
284
  the user's own ontology (`sessions-graph derive`).
295
- `[reconcile]` is omitted entirely from a freshly-bootstrapped file — absent
285
+ `[reconcile]` is omitted entirely from a file setup wrote — absent
296
286
  means "never configured," distinct from an explicit `auto_reconcile = false`.
297
287
  `[recall]` is optional too: without it, recall runs with the widths the
298
288
  benchmark measured; see [Recall](#recall-memory-for-the-harnesss-model).
@@ -311,14 +301,9 @@ memgraph-context-graph config set <key> <value>
311
301
  # recall.embedding_model, ontology.{path,derive}, memory.backend
312
302
  ```
313
303
 
314
- Environment variables (`MEMGRAPH_URL`, `MEMGRAPH_USER`, `MEMGRAPH_PASSWORD`, `MEMGRAPH_DATABASE`, `MEMGRAPH_CONTEXT_GRAPH_USER_ID`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) are consulted **only at bootstrap time** — if set, `bootstrap` persists them into the config file. Exporting them later has no effect on running hooks; use `config set` instead.
315
-
316
- `reconcile.auto_reconcile` is the one exception: `bootstrap` never captures
317
- `SESSIONS_GRAPH_AUTO_RECONCILE` from the environment, and re-running
318
- `bootstrap` preserves whatever it's currently set to rather than resetting it.
319
- Unlike the keys above, nobody has that env var exported for an unrelated
320
- reason — it's only ever set via `memgraph-context-graph config set
321
- reconcile.auto_reconcile true`.
304
+ Hooks never read environment variables for these values, and neither does setup: it writes only
305
+ `memgraph.url` and `identity.user_id`, from its flags or what it found. Everything else is set with
306
+ `config set`, and setup leaves it alone when it rewrites the file.
322
307
 
323
308
  ### Recall: memory for the harness's model
324
309
 
@@ -383,6 +368,9 @@ memgraph-context-graph setup claude-code --memory-backend context-graph
383
368
  memgraph-context-graph setup claude-code --memory-backend native # hand it back
384
369
  ```
385
370
 
371
+ (The interactive `setup` offers the same switch as a checkbox. `uninstall` turns the harness's own memory
372
+ back on, since without the plugin it would have none.)
373
+
386
374
  Opting in does three things, in this order:
387
375
  1. Imports the existing auto-memory files from every project once:
388
376
  - `user`/`feedback` memories go to `/memories/<type>/`;
@@ -477,7 +465,7 @@ Check the installed hook environment with:
477
465
  memgraph-context-graph doctor --runtime codex --connector skills-graph --connector actions-graph --connector sessions-graph
478
466
  ```
479
467
 
480
- Graph credentials live in `~/.config/context-graph/config.toml` (written by `bootstrap`/`config set`), not in plugin hook files or the process environment — hooks read that file at runtime. See [Configuration](#configuration).
468
+ 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).
481
469
 
482
470
  ### Claude Code Plugin
483
471
 
@@ -512,7 +500,7 @@ memgraph-context-graph doctor --runtime claude-code --connector skills-graph --c
512
500
  For source development and per-project experiments, you can generate local Codex hook files:
513
501
 
514
502
  ```bash
515
- memgraph-context-graph setup codex --project-dir "$PWD" --setup-schema
503
+ memgraph-context-graph hook init codex --project-dir "$PWD" --setup-schema
516
504
  ```
517
505
 
518
506
  This writes local, ignored files:
@@ -693,7 +681,7 @@ Then register it in your own package's `pyproject.toml` — this is the entire i
693
681
  my-runtime = "my_package.adapter:PLUGIN"
694
682
  ```
695
683
 
696
- Once installed, `memgraph-context-graph bootstrap --runtime my-runtime`, `doctor --runtime my-runtime`, `hook run my-runtime`, and `hook init my-runtime` (if `init` is implemented) work like the built-in registrations. See `runtime_plugin.py` for the full protocol and `pyproject.toml`'s own `[project.entry-points."memgraph_context_graph.runtimes"]` for registration examples.
684
+ Once installed, `doctor --runtime my-runtime`, `hook run my-runtime`, and `hook init my-runtime` (if `init` is implemented) work like the built-in registrations. See `runtime_plugin.py` for the full protocol and `pyproject.toml`'s own `[project.entry-points."memgraph_context_graph.runtimes"]` for registration examples.
697
685
 
698
686
  ### Adding a New Graph Component
699
687
 
@@ -24,7 +24,7 @@ uv tool install "memgraph-context-graph[mcp]" --with "skills-graph[memgraph-cont
24
24
 
25
25
  The `mcp` extra is what `memgraph-context-graph mcp` serves [recall](#recall-memory-for-the-harnesss-model) with.
26
26
 
27
- Or use the plugin bootstrap scripts; they fall back to `uvx` if the tool is not installed yet.
27
+ Or run `memgraph-context-graph setup`, which installs whatever connectors are missing (see [Setup](#setup)).
28
28
 
29
29
  For SDK usage inside an application:
30
30
 
@@ -153,7 +153,8 @@ Implemented:
153
153
  | Grok Build | `GrokHooksAdapter` | Command receives one JSON object on `stdin` |
154
154
 
155
155
  Codex and Claude Code have distributable Runtime Plugins. Copilot CLI, Cursor,
156
- OpenCode, Antigravity CLI, and Grok Build use project-local wiring generated by `hook init`:
156
+ OpenCode, Antigravity CLI, and Grok Build use project-local wiring, which `setup`
157
+ writes (or, one runtime at a time, `hook init`):
157
158
 
158
159
  ```bash
159
160
  memgraph-context-graph hook init copilot-cli
@@ -166,44 +167,32 @@ memgraph-context-graph hook init grok
166
167
  See the [command-hook reference](docs/command-hooks.md) for generated paths,
167
168
  payload differences, and current upstream hook documentation.
168
169
 
169
- ### First-Time Plugin Setup
170
+ ### Setup
170
171
 
171
- For Codex and Claude Code plugins, the recommended first-run path is the bootstrap command. It installs the runtime package (with the connector extras), checks Memgraph, and runs `doctor`.
172
-
173
- Prerequisites:
174
-
175
- - `uv` on `PATH`. (`uv` manages Python for the tool; if uv-managed Python downloads are blocked, install Python 3.10+ and rerun bootstrap.)
176
- - Memgraph running and reachable over Bolt. Defaults are `bolt://localhost:7687`, empty user/password, database `memgraph`. If it isn't running locally:
177
-
178
- ```bash
179
- docker run --rm -p 7687:7687 memgraph/memgraph-mage
180
- ```
181
-
182
- **1. Bootstrap all three connectors** (this is what the installed plugin wires into its hooks):
172
+ `memgraph-context-graph setup` installs Context Graph for every agent on the machine; the
173
+ [family README](../README.md#getting-started) walks through it. In short:
183
174
 
184
175
  ```bash
185
- # Codex
186
- memgraph-context-graph bootstrap --runtime codex \
187
- --connector skills-graph --connector actions-graph --connector sessions-graph
188
-
189
- # Claude Code
190
- memgraph-context-graph bootstrap --runtime claude-code \
191
- --connector skills-graph --connector actions-graph --connector sessions-graph
176
+ memgraph-context-graph setup # interactive on a terminal; plain otherwise
177
+ memgraph-context-graph setup claude-code codex # just these agents
178
+ memgraph-context-graph setup cursor --project-dir ~/work/billing
179
+ memgraph-context-graph setup --plan # print the plan, change nothing
180
+ memgraph-context-graph uninstall [runtime ...] # remove the wiring; the graph and config stay
192
181
  ```
193
182
 
194
- The plugin wrapper script runs the same command (and falls back to `uvx` if the tool isn't installed yet):
195
-
196
- ```bash
197
- ./scripts/bootstrap.sh
198
- ```
183
+ It finds or starts Memgraph (a `memgraph-mage` container with a volume, on a free port when 7687 is taken),
184
+ writes `memgraph.url` and `identity.user_id`, installs missing connectors and the viewer into the uv tool,
185
+ wires each agent, and runs `doctor`'s checks. Every step checks before it acts, so rerunning only does what's
186
+ missing. The TUI starts only when stdin and stdout are both terminals; the plugins' `scripts/bootstrap.sh`
187
+ runs `setup <runtime> --no-tui`.
199
188
 
200
- **2. Configure identity and connection.** Bootstrap writes `~/.config/context-graph/config.toml`; hooks read their configuration from that file at runtime (see [Configuration](#configuration) — env vars are **not** read at hook time). Set your identity, which sessions-graph requires:
189
+ **Identity and connection.** setup writes `~/.config/context-graph/config.toml`; hooks read their configuration from that file at runtime (see [Configuration](#configuration) — env vars are **not** read at hook time). To change your identity later:
201
190
 
202
191
  ```bash
203
192
  memgraph-context-graph config set identity.user_id "your-name"
204
193
  ```
205
194
 
206
- The Memgraph connection defaults to `bolt://localhost:7687`. For a remote or HA instance:
195
+ For a remote or HA instance, pass `setup --memgraph-url`, or set it with credentials afterwards:
207
196
 
208
197
  ```bash
209
198
  memgraph-context-graph config set memgraph.url "neo4j://<coordinator-host>:7687"
@@ -212,7 +201,7 @@ memgraph-context-graph config set memgraph.password # prompts; stored 060
212
201
  memgraph-context-graph config set memgraph.database "memgraph"
213
202
  ```
214
203
 
215
- **3. Verify:**
204
+ **Verify** (setup runs the same checks):
216
205
 
217
206
  ```bash
218
207
  memgraph-context-graph config show
@@ -267,7 +256,7 @@ turns_k = "8"
267
256
  [sessions-graph § reconciliation](../sessions-graph/README.md#session-reconciliation)),
268
257
  which needs no LLM. `[llm]` is optional: a key only lets reconciliation learn
269
258
  the user's own ontology (`sessions-graph derive`).
270
- `[reconcile]` is omitted entirely from a freshly-bootstrapped file — absent
259
+ `[reconcile]` is omitted entirely from a file setup wrote — absent
271
260
  means "never configured," distinct from an explicit `auto_reconcile = false`.
272
261
  `[recall]` is optional too: without it, recall runs with the widths the
273
262
  benchmark measured; see [Recall](#recall-memory-for-the-harnesss-model).
@@ -286,14 +275,9 @@ memgraph-context-graph config set <key> <value>
286
275
  # recall.embedding_model, ontology.{path,derive}, memory.backend
287
276
  ```
288
277
 
289
- Environment variables (`MEMGRAPH_URL`, `MEMGRAPH_USER`, `MEMGRAPH_PASSWORD`, `MEMGRAPH_DATABASE`, `MEMGRAPH_CONTEXT_GRAPH_USER_ID`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) are consulted **only at bootstrap time** — if set, `bootstrap` persists them into the config file. Exporting them later has no effect on running hooks; use `config set` instead.
290
-
291
- `reconcile.auto_reconcile` is the one exception: `bootstrap` never captures
292
- `SESSIONS_GRAPH_AUTO_RECONCILE` from the environment, and re-running
293
- `bootstrap` preserves whatever it's currently set to rather than resetting it.
294
- Unlike the keys above, nobody has that env var exported for an unrelated
295
- reason — it's only ever set via `memgraph-context-graph config set
296
- reconcile.auto_reconcile true`.
278
+ Hooks never read environment variables for these values, and neither does setup: it writes only
279
+ `memgraph.url` and `identity.user_id`, from its flags or what it found. Everything else is set with
280
+ `config set`, and setup leaves it alone when it rewrites the file.
297
281
 
298
282
  ### Recall: memory for the harness's model
299
283
 
@@ -358,6 +342,9 @@ memgraph-context-graph setup claude-code --memory-backend context-graph
358
342
  memgraph-context-graph setup claude-code --memory-backend native # hand it back
359
343
  ```
360
344
 
345
+ (The interactive `setup` offers the same switch as a checkbox. `uninstall` turns the harness's own memory
346
+ back on, since without the plugin it would have none.)
347
+
361
348
  Opting in does three things, in this order:
362
349
  1. Imports the existing auto-memory files from every project once:
363
350
  - `user`/`feedback` memories go to `/memories/<type>/`;
@@ -452,7 +439,7 @@ Check the installed hook environment with:
452
439
  memgraph-context-graph doctor --runtime codex --connector skills-graph --connector actions-graph --connector sessions-graph
453
440
  ```
454
441
 
455
- Graph credentials live in `~/.config/context-graph/config.toml` (written by `bootstrap`/`config set`), not in plugin hook files or the process environment — hooks read that file at runtime. See [Configuration](#configuration).
442
+ 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).
456
443
 
457
444
  ### Claude Code Plugin
458
445
 
@@ -487,7 +474,7 @@ memgraph-context-graph doctor --runtime claude-code --connector skills-graph --c
487
474
  For source development and per-project experiments, you can generate local Codex hook files:
488
475
 
489
476
  ```bash
490
- memgraph-context-graph setup codex --project-dir "$PWD" --setup-schema
477
+ memgraph-context-graph hook init codex --project-dir "$PWD" --setup-schema
491
478
  ```
492
479
 
493
480
  This writes local, ignored files:
@@ -668,7 +655,7 @@ Then register it in your own package's `pyproject.toml` — this is the entire i
668
655
  my-runtime = "my_package.adapter:PLUGIN"
669
656
  ```
670
657
 
671
- Once installed, `memgraph-context-graph bootstrap --runtime my-runtime`, `doctor --runtime my-runtime`, `hook run my-runtime`, and `hook init my-runtime` (if `init` is implemented) work like the built-in registrations. See `runtime_plugin.py` for the full protocol and `pyproject.toml`'s own `[project.entry-points."memgraph_context_graph.runtimes"]` for registration examples.
658
+ Once installed, `doctor --runtime my-runtime`, `hook run my-runtime`, and `hook init my-runtime` (if `init` is implemented) work like the built-in registrations. See `runtime_plugin.py` for the full protocol and `pyproject.toml`'s own `[project.entry-points."memgraph_context_graph.runtimes"]` for registration examples.
672
659
 
673
660
  ### Adding a New Graph Component
674
661
 
@@ -234,14 +234,11 @@ memgraph-context-graph config set memgraph.url "bolt://localhost:7687"
234
234
  memgraph-context-graph config show
235
235
  ```
236
236
 
237
- Bootstrap captures supported environment variables into that file as a
238
- write-time convenience:
237
+ `memgraph-context-graph setup` writes `memgraph.url` and `identity.user_id` there,
238
+ and wires the project in one go:
239
239
 
240
240
  ```bash
241
- memgraph-context-graph bootstrap --runtime antigravity-cli \
242
- --connector skills-graph \
243
- --connector actions-graph \
244
- --connector sessions-graph
241
+ memgraph-context-graph setup antigravity-cli --project-dir "$PWD"
245
242
  ```
246
243
 
247
244
  ## Verification
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memgraph-context-graph"
3
- version = "0.5.0"
3
+ version = "0.6.0"
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" }
@@ -15,6 +15,9 @@ dependencies = [
15
15
  "memgraph-toolbox>=0.1.11",
16
16
  # Edits ~/.codex/config.toml when switching Codex's memory, keeping the user's comments and layout.
17
17
  "tomlkit>=0.13,<1",
18
+ # setup's interactive installer. Imported only when it runs, so hooks never load it; pinned to the
19
+ # major the viewer uses, since Textual's API moves between majors.
20
+ "textual>=8.2,<9",
18
21
  ]
19
22
 
20
23
  [project.scripts]
@@ -45,6 +48,11 @@ test = [
45
48
  "pytest-asyncio>=0.24.0",
46
49
  ]
47
50
 
51
+ [tool.pytest.ini_options]
52
+ markers = [
53
+ "docker: starts real Memgraph containers, so it needs Docker; CI runs these in their own job",
54
+ ]
55
+
48
56
  [build-system]
49
57
  requires = ["hatchling"]
50
58
  build-backend = "hatchling.build"
@@ -49,7 +49,12 @@ _DEFAULT_CONFIG_DIR = Path.home() / ".config" / "context-graph"
49
49
  def config_file() -> Path:
50
50
  """The config file hooks read, honouring :data:`CONFIG_PATH_ENV`."""
51
51
  override = os.environ.get(CONFIG_PATH_ENV)
52
- return Path(override) if override else _DEFAULT_CONFIG_DIR / "config.toml"
52
+ return Path(override) if override else default_config_file()
53
+
54
+
55
+ def default_config_file() -> Path:
56
+ """The config file used when :data:`CONFIG_PATH_ENV` is unset."""
57
+ return _DEFAULT_CONFIG_DIR / "config.toml"
53
58
 
54
59
 
55
60
  # Defaults matching memgraph-toolbox's MEMGRAPH_ENV_DEFAULTS.
@@ -116,6 +121,10 @@ class HookConfig:
116
121
  #: "left at the default" from "written": Enterprise refuses an explicit ``identity.user_id`` or
117
122
  #: ``memgraph.database``, which would quietly override what the login decides.
118
123
  present: frozenset[str] = frozenset()
124
+ #: Why the file could not be read as written: unreadable, or lines that are not ``key = value``.
125
+ #: Values those lines meant to set fell back to defaults, which can point hooks at the wrong
126
+ #: Memgraph, so hooks log these to the capture-failure log and ``doctor`` fails on them.
127
+ problems: tuple[str, ...] = ()
119
128
 
120
129
  @property
121
130
  def graph_memory(self) -> bool:
@@ -321,18 +330,11 @@ def write_full_config(
321
330
  anthropic_api_key: str = _LLM_DEFAULTS["anthropic_api_key"],
322
331
  auto_reconcile: bool | None = _RECONCILE_DEFAULTS["auto_reconcile"],
323
332
  ) -> Path:
324
- """Write a complete config file with all sections (used by bootstrap).
325
-
326
- Overwrites every section except ``[reconcile]``, ``[recall]``, ``[ontology]``, ``[github]`` and ``[memory]``: unlike identity/Memgraph/LLM
327
- settings, ``auto_reconcile`` has no legitimate ambient-env source for
328
- ``bootstrap`` to capture (nobody has ``SESSIONS_GRAPH_AUTO_RECONCILE``
329
- exported for an unrelated reason the way they might already have
330
- ``OPENAI_API_KEY``/`MEMGRAPH_PASSWORD` set) — it is only ever set via
331
- ``config set reconcile.auto_reconcile``. Re-running bootstrap must not
332
- silently revert it to off, so ``auto_reconcile`` is preserved from the
333
- existing file unless explicitly given here. ``[recall]``, ``[ontology]`` and
334
- ``[github]`` are preserved the same way: they are only ever set via ``config set`` or by
335
- editing the file. ``[memory]`` is set by ``setup --memory-backend`` and kept likewise.
333
+ """Write a complete config file, resetting identity, Memgraph and LLM settings to what is given.
334
+
335
+ Every other section (``[reconcile]``, ``[recall]``, ``[ontology]``, ``[github]``, ``[memory]``) and the
336
+ edition are kept from the existing file, since only ``config set`` or a hand edit ever sets them.
337
+ ``auto_reconcile`` is kept likewise unless given here. :func:`write_config` is the partial update.
336
338
  """
337
339
  global _cached_config
338
340
 
@@ -380,14 +382,19 @@ def config_dir_path() -> Path:
380
382
 
381
383
 
382
384
  def _read_config_file() -> HookConfig:
383
- """Parse the config file. Returns defaults if file is missing or malformed."""
384
- if not config_file().is_file():
385
- return HookConfig()
385
+ """Parse the config file.
386
386
 
387
+ A missing file gives defaults. An unreadable or partly unparsable one also
388
+ gives defaults for whatever it failed to set, never an exception (a hook
389
+ must not crash on its config), and says so in :attr:`HookConfig.problems`.
390
+ """
391
+ problems: list[str] = []
387
392
  try:
388
- sections = _parse_toml(config_file())
389
- except Exception:
390
- return HookConfig()
393
+ if not config_file().is_file():
394
+ return HookConfig()
395
+ sections = _parse_toml(config_file(), problems)
396
+ except Exception as exc:
397
+ return HookConfig(problems=(f"unreadable: {type(exc).__name__}: {exc}",))
391
398
 
392
399
  identity = sections.get("identity", {})
393
400
  memgraph = sections.get("memgraph", {})
@@ -421,19 +428,21 @@ def _read_config_file() -> HookConfig:
421
428
  for key, value in keys.items()
422
429
  if value
423
430
  ),
431
+ problems=tuple(problems),
424
432
  )
425
433
 
426
434
 
427
- def _parse_toml(path: Path) -> dict[str, dict[str, str]]:
435
+ def _parse_toml(path: Path, problems: list[str] | None = None) -> dict[str, dict[str, str]]:
428
436
  """Minimal TOML parser — handles [section] and key = "value" pairs.
429
437
 
430
438
  Only supports string values (quoted or bare). Sufficient for our config shape.
431
- Keys before the first section header land in the ``""`` section.
439
+ Keys before the first section header land in the ``""`` section. A line that
440
+ is none of these is skipped and, when *problems* is given, reported there.
432
441
  """
433
442
  sections: dict[str, dict[str, str]] = {"": {}}
434
443
  current_section = ""
435
444
 
436
- for line in path.read_text(encoding="utf-8").splitlines():
445
+ for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
437
446
  stripped = line.strip()
438
447
  if not stripped or stripped.startswith("#"):
439
448
  continue
@@ -441,15 +450,18 @@ def _parse_toml(path: Path) -> dict[str, dict[str, str]]:
441
450
  current_section = stripped[1:-1].strip()
442
451
  sections.setdefault(current_section, {})
443
452
  continue
444
- if "=" in stripped:
445
- key, _, value = stripped.partition("=")
446
- key = key.strip()
447
- value = value.strip()
448
- if value[:1] not in {'"', "'"} and "#" in value:
449
- # Bare (unquoted) value with a trailing comment, e.g. `auto_reconcile = true # off for now`.
450
- value = value.partition("#")[0].strip()
451
- value = value.strip('"').strip("'")
452
- sections[current_section][key] = value
453
+ if "=" not in stripped or not stripped.partition("=")[0].strip():
454
+ if problems is not None:
455
+ problems.append(f"line {number} is not `key = value` and was ignored")
456
+ continue
457
+ key, _, value = stripped.partition("=")
458
+ key = key.strip()
459
+ value = value.strip()
460
+ if value[:1] not in {'"', "'"} and "#" in value:
461
+ # Bare (unquoted) value with a trailing comment, e.g. `auto_reconcile = true # off for now`.
462
+ value = value.partition("#")[0].strip()
463
+ value = value.strip('"').strip("'")
464
+ sections[current_section][key] = value
453
465
 
454
466
  return sections
455
467
 
@@ -298,7 +298,9 @@ def write_hook_config(
298
298
  """Write a runtime's project-local JSON hook configuration.
299
299
 
300
300
  Existing settings files are merged only when the runtime declares
301
- ``merge=True``. Otherwise callers must pass ``force=True`` to replace one.
301
+ ``merge=True``: every hook entry this runtime's command didn't write is
302
+ kept, and this runtime's own entries are replaced, so writing twice leaves
303
+ one copy. Otherwise callers must pass ``force=True`` to replace the file.
302
304
 
303
305
  Raises:
304
306
  ValueError: if *spec* declares no config path, or an existing file to
@@ -326,13 +328,82 @@ def write_hook_config(
326
328
  existing_hooks = document.get(spec.config.root_key, {})
327
329
  if not isinstance(existing_hooks, dict):
328
330
  raise ValueError(f"Expected {spec.config.root_key!r} to be an object in {config_path}")
329
- document[spec.config.root_key] = {**existing_hooks, **hooks}
331
+ merged = _without_own_entries(spec, existing_hooks)
332
+ for event, entries in hooks.items():
333
+ merged[event] = [*merged.get(event, []), *entries]
334
+ document[spec.config.root_key] = merged
330
335
 
331
336
  config_path.parent.mkdir(parents=True, exist_ok=True)
332
337
  config_path.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8")
333
338
  return config_path
334
339
 
335
340
 
341
+ def has_hook_config(spec: RuntimeSpec, project_dir: Path) -> bool:
342
+ """Whether *project_dir*'s hook file for *spec* holds an entry this runtime's command wrote."""
343
+ hooks = _read_hooks(spec, project_dir)
344
+ return hooks is not None and any(_is_own_entry(spec, entry) for entries in hooks.values() for entry in entries)
345
+
346
+
347
+ def remove_hook_config(spec: RuntimeSpec, project_dir: Path) -> bool:
348
+ """Remove this runtime's entries from *project_dir*'s hook file, keeping everything else.
349
+
350
+ A file left with nothing but an empty hook map (and ``version``) is deleted. Returns whether anything
351
+ was removed.
352
+
353
+ Raises:
354
+ ValueError: if the file exists but isn't a JSON object with an object-valued hook map.
355
+ """
356
+ if not has_hook_config(spec, project_dir):
357
+ return False
358
+ config_path = project_dir / str(spec.config.path)
359
+ document = json.loads(config_path.read_text(encoding="utf-8"))
360
+ remaining = _without_own_entries(spec, document[spec.config.root_key])
361
+ if remaining:
362
+ document[spec.config.root_key] = remaining
363
+ else:
364
+ del document[spec.config.root_key]
365
+ if set(document) <= {"version"}:
366
+ config_path.unlink()
367
+ else:
368
+ config_path.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8")
369
+ return True
370
+
371
+
372
+ def _read_hooks(spec: RuntimeSpec, project_dir: Path) -> dict[str, list[Any]] | None:
373
+ if spec.config.path is None:
374
+ return None
375
+ config_path = project_dir / spec.config.path
376
+ if not config_path.is_file():
377
+ return None
378
+ document = json.loads(config_path.read_text(encoding="utf-8"))
379
+ if not isinstance(document, dict) or not isinstance(document.get(spec.config.root_key, {}), dict):
380
+ raise ValueError(f"Expected a JSON object with an object {spec.config.root_key!r} in {config_path}")
381
+ return {
382
+ event: entries for event, entries in document.get(spec.config.root_key, {}).items() if isinstance(entries, list)
383
+ }
384
+
385
+
386
+ def _without_own_entries(spec: RuntimeSpec, hooks: dict[str, Any]) -> dict[str, Any]:
387
+ """*hooks* minus this runtime's own entries; events left empty are dropped."""
388
+ kept: dict[str, Any] = {}
389
+ for event, entries in hooks.items():
390
+ if not isinstance(entries, list):
391
+ kept[event] = entries
392
+ continue
393
+ others = [entry for entry in entries if not _is_own_entry(spec, entry)]
394
+ if others:
395
+ kept[event] = others
396
+ return kept
397
+
398
+
399
+ def _is_own_entry(spec: RuntimeSpec, entry: Any) -> bool:
400
+ """Whether a hook entry (flat, or a matcher group holding ``hooks``) runs this runtime's capture command."""
401
+ marker = f"hook run {spec.name}"
402
+ if isinstance(entry, dict) and isinstance(entry.get("hooks"), list):
403
+ return any(_is_own_entry(spec, inner) for inner in entry["hooks"])
404
+ return isinstance(entry, dict) and marker in str(entry.get(spec.config.command_key, ""))
405
+
406
+
336
407
  def hook_command_argv(runtime: str, connectors: list[str]) -> list[str]:
337
408
  """Build the installed hook command for *runtime* and *connectors* as argv."""
338
409
  executable = shutil.which("memgraph-context-graph")
@@ -131,7 +131,7 @@ class OpenCodeHooksAdapter(SpecAdapter):
131
131
  SPEC = SPEC
132
132
 
133
133
 
134
- _PLUGIN_PATH = ".opencode/plugins/memgraph-context-graph/index.js"
134
+ PLUGIN_PATH = ".opencode/plugins/memgraph-context-graph/index.js"
135
135
 
136
136
 
137
137
  def init(
@@ -151,7 +151,7 @@ def init(
151
151
  Raises:
152
152
  FileExistsError: if the plugin file exists and *force* is false.
153
153
  """
154
- target = project_dir / _PLUGIN_PATH
154
+ target = project_dir / PLUGIN_PATH
155
155
  if target.exists() and not force:
156
156
  raise FileExistsError(f"Refusing to overwrite existing OpenCode plugin: {target} (pass force=True)")
157
157