memgraph-context-graph 0.12.2__tar.gz → 0.12.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/CONTEXT.md +8 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/PKG-INFO +1 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/memory.md +20 -4
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/runtime-setup.md +5 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/sdk-memory.md +4 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/pyproject.toml +1 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/_identity.py +2 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/_spec.py +12 -3
- memgraph_context_graph-0.12.4/src/memgraph_context_graph/adapters/_text_file.py +23 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/agent_mcp.py +2 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/claude_code_memory.py +26 -10
- memgraph_context_graph-0.12.4/src/memgraph_context_graph/adapters/codex.py +248 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/opencode.py +2 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/cli.py +30 -2
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/connectors.py +1 -1
- memgraph_context_graph-0.12.4/src/memgraph_context_graph/graph_version.py +327 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/hooks/runner.py +60 -6
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/mcp_server.py +18 -3
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/memory_backend.py +5 -1
- memgraph_context_graph-0.12.4/src/memgraph_context_graph/refused_events.py +317 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/sdk/__init__.py +32 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/session.py +16 -3
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/setup/cli.py +4 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/setup/tool.py +58 -8
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/setup/update.py +24 -1
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/tools.py +49 -15
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/conftest.py +11 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_codex_adapter.py +107 -0
- memgraph_context_graph-0.12.4/tests/test_graph_version.py +249 -0
- memgraph_context_graph-0.12.4/tests/test_refused_events.py +335 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_session.py +9 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_setup.py +41 -0
- memgraph_context_graph-0.12.4/tests/test_text_file.py +38 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_tools.py +58 -0
- memgraph_context_graph-0.12.2/src/memgraph_context_graph/adapters/codex.py +0 -121
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/.gitignore +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/LICENSE +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/README.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/adr/0003-config-path-override.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/adr/0007-spawned-processes-read-the-config-file-themselves.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/command-hooks.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/configuration.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/extending.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/docs/research/2026-10-codex-memory.md +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/examples/sdk_claude_agent.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/examples/sdk_openai_agents.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/agent_memory.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/antigravity_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/claude_code.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/codex_memory.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/copilot_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/cursor.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/adapters/grok.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/capture_log.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/capture_policy.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/config_schema.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/events.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/forget.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/hooks/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/hooks/cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/hooks/runtime_plugin.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/link.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/protocols.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/sdk/claude.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/sdk/openai.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/setup/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/setup/agents.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/setup/engine.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/setup/memgraph.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/setup/prompts.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/setup/uninstall.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/spaces.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/terminal.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/__init__.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/fake_agent_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_agent_memory_wiring.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_antigravity_cli_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_capture_log.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_capture_policy.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_claude_code_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_claude_code_memory.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_codex_memory.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_config_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_connectors.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_context_before_tool.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_copilot_cli_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_cursor_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_dev_scripts.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_events.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_grok_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_hook_cli.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_identity.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_link.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_opencode_adapter.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_plugin_hooks.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_runtime_plugin.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_setup_docker.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_spaces.py +0 -0
- {memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/tests/test_terminal.py +0 -0
|
@@ -78,6 +78,14 @@ _Avoid_: MCP tool (the protocol it is served over, not the thing), Graph Connect
|
|
|
78
78
|
`ConnectorSpec` (`connectors.py`): a graph component's name, its graph class, how to build its **Graph Connector** from **Hook Configuration**, and an optional `doctor` check. Published via the `memgraph_context_graph.connectors` entry-point group; the hook runner, schema setup and `doctor` find connectors only through it. The hub keeps just what it needs before a component is installed: each installable connector's requirement (`INSTALLABLE`) and the default set.
|
|
79
79
|
_Avoid_: Component (the package), Graph Connector (the object a registration builds)
|
|
80
80
|
|
|
81
|
+
**Graph Epoch**:
|
|
82
|
+
The version of one component's shape in the graph, an integer that only a migration raises; every graph is at epoch 1 until one does. The graph states per component the oldest epoch that may write and read it, `(:ContextGraphSchema {component, epoch, min_writer_epoch, min_reader_epoch})`, and every client checks that before writing or reading (`graph_version.py`): a client older than the graph stops, never corrupts. No node means no constraint. Clients record themselves as `(:ContextGraphClient)`, so a migration can see who still writes an old shape. Not a package version: a release that leaves the graph's shape alone keeps the epoch.
|
|
83
|
+
_Avoid_: schema version, package version, migration (raises an epoch; isn't one)
|
|
84
|
+
|
|
85
|
+
**Refused-Event Spool**:
|
|
86
|
+
Where a connector's events go while the graph refuses it (**Graph Epoch**): a local, 0600, size-capped JSONL file beside the capture-failure log (`refused_events.py`), holding each event as the capture policy left it, original timestamp included. `update` replays it into the connectors the graph allows again. Its line format is versioned and read by later releases, so a change keeps a reader for every earlier format.
|
|
87
|
+
_Avoid_: queue, buffer, outbox (nothing sends it on its own; `update` replays it)
|
|
88
|
+
|
|
81
89
|
## Relationships
|
|
82
90
|
|
|
83
91
|
- **Memgraph Context Graph** belongs to broader **Context Graph** family.
|
|
@@ -93,7 +93,11 @@ Opting in does three things, in this order:
|
|
|
93
93
|
project already put a different memory of the same name there, this one
|
|
94
94
|
goes to its own project's folder instead, so neither is lost;
|
|
95
95
|
- `project`/`reference` memories go to that project's folder;
|
|
96
|
-
- `MEMORY.md` is skipped, and anything already in the graph is never overwritten
|
|
96
|
+
- `MEMORY.md` is skipped, and anything already in the graph is never overwritten;
|
|
97
|
+
- each file remembers the project it came from and keeps its own
|
|
98
|
+
modification time, so the session-start index can still put a project's
|
|
99
|
+
`user`/`feedback` memories with that project. Running the switch again
|
|
100
|
+
records this for files an earlier import brought in.
|
|
97
101
|
2. Sets `autoMemoryEnabled: false` in `~/.claude/settings.json`, the user-level
|
|
98
102
|
settings file, because a plugin can't set it itself. The file is backed up
|
|
99
103
|
once to `settings.json.context-graph.bak`.
|
|
@@ -122,8 +126,8 @@ its global store, with three differences:
|
|
|
122
126
|
compaction (`compact`), so the memory index comes back once the context is
|
|
123
127
|
summarized.
|
|
124
128
|
|
|
125
|
-
Codex caps hook context at about 2,500 tokens, so
|
|
126
|
-
|
|
129
|
+
Codex caps hook context at about 2,500 tokens, so its session-start text is
|
|
130
|
+
kept to 9,000 characters, with pinned files before the index. See
|
|
127
131
|
[the Codex research note](research/2026-10-codex-memory.md).
|
|
128
132
|
|
|
129
133
|
If the import can't run, for example because Memgraph is down, nothing is
|
|
@@ -192,10 +196,22 @@ change shows from the next session.
|
|
|
192
196
|
At session start the hook loads the memory, the way the harness's built-in
|
|
193
197
|
memory would have. It adds:
|
|
194
198
|
- an index of the root and the current project's files, one line each with
|
|
195
|
-
the file's `description:` frontmatter or first line, up to 200 lines
|
|
199
|
+
the file's `description:` frontmatter or first line, up to 200 lines. The
|
|
200
|
+
current project's files come first, then root files from this project
|
|
201
|
+
(imported from its memory, or written in its sessions by an SDK app), then
|
|
202
|
+
the rest, newest first, so an import of every project's `user`/`feedback`
|
|
203
|
+
memories can't crowd this project's out;
|
|
196
204
|
- the full text of any file with `pin: true` in its frontmatter;
|
|
197
205
|
- the current project's folder.
|
|
198
206
|
|
|
207
|
+
All of that, with recall's "where you left off", fits in what the agent shows
|
|
208
|
+
its model at session start: 10,000 characters, Claude Code's cap on a hook's
|
|
209
|
+
context (past it, the model sees only a 2,000-character preview), and 9,000 for
|
|
210
|
+
Codex. Pinned text takes at most half. The index lists every file by its path
|
|
211
|
+
before it describes any, then gives descriptions in its order, so the current
|
|
212
|
+
project's files keep theirs longest. When even the paths don't fit, it ends
|
|
213
|
+
with how many it left out and where to list them.
|
|
214
|
+
|
|
199
215
|
The index is generated from the graph, so the model never maintains an index
|
|
200
216
|
file and it can't go stale. The project folder is keyed by the checkout's git
|
|
201
217
|
`origin` remote (e.g. `github.com_memgraph_ai-toolkit`), so every clone of a
|
|
@@ -76,6 +76,11 @@ and constraints. It then runs `doctor`'s checks. Name runtimes to refresh only t
|
|
|
76
76
|
and the graph's data are untouched, and running it again changes nothing.
|
|
77
77
|
Running agent sessions keep the hooks they loaded: restart them. Outside the uv tool (a virtualenv, say) the
|
|
78
78
|
package step only warns, and you upgrade it yourself first.
|
|
79
|
+
Once a newer Context Graph has reshaped your graph, an older install stops writing (and, if needed, reading) the
|
|
80
|
+
parts it no longer knows instead of corrupting them: the session-start context, the capture-failure log and
|
|
81
|
+
`doctor` say so, and `update` brings it level. What the agents did meanwhile isn't lost: it is kept, redacted
|
|
82
|
+
like any capture, in `refused-events.jsonl` beside the capture-failure log (readable only by you, at most 64 MB),
|
|
83
|
+
and `update` adds it to the graph with its original times.
|
|
79
84
|
|
|
80
85
|
**Identity and connection.** setup writes `~/.config/context-graph/config.toml`; hooks read their configuration from that file at runtime (see [Configuration](configuration.md) — env vars are **not** read at hook time). To change your identity later:
|
|
81
86
|
|
|
@@ -31,6 +31,10 @@ from memgraph_context_graph.sdk import ContextGraphMemory
|
|
|
31
31
|
memory = ContextGraphMemory(user_id="alice")
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
A long-running app keeps working across a Context Graph upgrade elsewhere. About every ten minutes it checks whether
|
|
35
|
+
a newer Context Graph has reshaped the graph. If it has, the app stops writing the parts it no longer knows, keeps
|
|
36
|
+
those events on the machine, and `memgraph-context-graph update` adds them once this install is upgraded.
|
|
37
|
+
|
|
34
38
|
## Claude Agent SDK
|
|
35
39
|
|
|
36
40
|
The SDK's hook callbacks get Claude Code's own hook payloads, so the `claude-code` rules translate them, under `source_sdk = "claude-agent-sdk"`. Python callbacks get no `SessionStart` or `SessionEnd`, which changes two things:
|
|
@@ -30,6 +30,7 @@ from dataclasses import dataclass, field, replace
|
|
|
30
30
|
from pathlib import Path
|
|
31
31
|
from typing import Any
|
|
32
32
|
|
|
33
|
+
from memgraph_context_graph.adapters._text_file import write_text_keeping_newlines
|
|
33
34
|
from memgraph_context_graph.config_schema import (
|
|
34
35
|
CONFIG_KEYS,
|
|
35
36
|
SECTIONS,
|
|
@@ -355,7 +356,7 @@ def _write_config_file(content: str) -> Path:
|
|
|
355
356
|
global _cached_config
|
|
356
357
|
path = config_file()
|
|
357
358
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
358
|
-
path
|
|
359
|
+
write_text_keeping_newlines(path, content)
|
|
359
360
|
path.chmod(stat.S_IRUSR | stat.S_IWUSR) # 0600: the file holds credentials.
|
|
360
361
|
_cached_config = None
|
|
361
362
|
return path
|
|
@@ -19,6 +19,7 @@ from dataclasses import dataclass, field
|
|
|
19
19
|
from typing import TYPE_CHECKING, Any, Literal
|
|
20
20
|
|
|
21
21
|
from memgraph_context_graph.adapters._identity import resolve_user_id
|
|
22
|
+
from memgraph_context_graph.adapters._text_file import write_text_keeping_newlines
|
|
22
23
|
from memgraph_context_graph.events import (
|
|
23
24
|
AgentEndEvent,
|
|
24
25
|
AgentStartEvent,
|
|
@@ -30,6 +31,7 @@ from memgraph_context_graph.events import (
|
|
|
30
31
|
TurnEndEvent,
|
|
31
32
|
)
|
|
32
33
|
from memgraph_context_graph.protocols import RuntimeAdapter
|
|
34
|
+
from memgraph_context_graph.tools import SESSION_CONTEXT_CHARS
|
|
33
35
|
|
|
34
36
|
if TYPE_CHECKING:
|
|
35
37
|
from pathlib import Path
|
|
@@ -115,7 +117,8 @@ class RuntimeSpec:
|
|
|
115
117
|
modifying the call. ``session_context`` renders the stdout JSON that puts
|
|
116
118
|
the session-start text (memory index, where the user left off) in front of
|
|
117
119
|
the model when ``session_context_event`` fires; None where no hook output
|
|
118
|
-
reaches the model at session start.
|
|
120
|
+
reaches the model at session start. ``session_context_limit`` is how many
|
|
121
|
+
characters of that text the runtime shows its model.
|
|
119
122
|
"""
|
|
120
123
|
|
|
121
124
|
name: str
|
|
@@ -133,6 +136,7 @@ class RuntimeSpec:
|
|
|
133
136
|
context_before_tool: Callable[[str], Mapping[str, Any]] | None = None
|
|
134
137
|
session_context: Callable[[str], Mapping[str, Any]] | None = None
|
|
135
138
|
session_context_event: str = "SessionStart"
|
|
139
|
+
session_context_limit: int = SESSION_CONTEXT_CHARS
|
|
136
140
|
|
|
137
141
|
def event_name(self, payload: Mapping[str, Any]) -> str:
|
|
138
142
|
"""Return *payload*'s canonical event name."""
|
|
@@ -251,6 +255,11 @@ class SpecPlugin:
|
|
|
251
255
|
"""Return the runtime's registered name."""
|
|
252
256
|
return self.spec.name
|
|
253
257
|
|
|
258
|
+
@property
|
|
259
|
+
def session_context_limit(self) -> int:
|
|
260
|
+
"""Return how many characters of session-start text the runtime shows its model."""
|
|
261
|
+
return self.spec.session_context_limit
|
|
262
|
+
|
|
254
263
|
@property
|
|
255
264
|
def probe_payload(self) -> Mapping[str, Any]:
|
|
256
265
|
"""Return the payload ``doctor`` replays to smoke-test the runtime."""
|
|
@@ -377,7 +386,7 @@ def write_hook_config(
|
|
|
377
386
|
document[spec.config.root_key] = merged
|
|
378
387
|
|
|
379
388
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
380
|
-
config_path
|
|
389
|
+
write_text_keeping_newlines(config_path, json.dumps(document, indent=2) + "\n")
|
|
381
390
|
return config_path
|
|
382
391
|
|
|
383
392
|
|
|
@@ -408,7 +417,7 @@ def remove_hook_config(spec: RuntimeSpec, project_dir: Path) -> bool:
|
|
|
408
417
|
if set(document) <= {"version"}:
|
|
409
418
|
config_path.unlink()
|
|
410
419
|
else:
|
|
411
|
-
config_path
|
|
420
|
+
write_text_keeping_newlines(config_path, json.dumps(document, indent=2) + "\n")
|
|
412
421
|
return True
|
|
413
422
|
|
|
414
423
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Writing a user's files without changing their line endings.
|
|
2
|
+
|
|
3
|
+
``Path.write_text`` turns ``\\n`` into the platform's line ending, so on Windows every file setup or
|
|
4
|
+
the memory switch rewrote came back CRLF, even when its content didn't change. A rewrite here keeps
|
|
5
|
+
the ending the file already had, and a new file gets ``\\n`` on every platform.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_text_keeping_newlines(path: Path, text: str) -> None:
|
|
17
|
+
"""Write *text* to *path* as UTF-8 with the line ending *path* already uses.
|
|
18
|
+
|
|
19
|
+
*text* may use ``\\n`` or ``\\r\\n``; it is written with ``\\r\\n`` only if the existing file holds one,
|
|
20
|
+
else with ``\\n``.
|
|
21
|
+
"""
|
|
22
|
+
newline = "\r\n" if path.is_file() and b"\r\n" in path.read_bytes() else "\n"
|
|
23
|
+
path.write_text(text.replace("\r\n", "\n"), encoding="utf-8", newline=newline)
|
|
@@ -27,6 +27,7 @@ import sys
|
|
|
27
27
|
from dataclasses import dataclass
|
|
28
28
|
from typing import TYPE_CHECKING, Any
|
|
29
29
|
|
|
30
|
+
from memgraph_context_graph.adapters._text_file import write_text_keeping_newlines
|
|
30
31
|
from memgraph_context_graph.memory_backend import MemoryBackendError
|
|
31
32
|
|
|
32
33
|
if TYPE_CHECKING:
|
|
@@ -322,5 +323,5 @@ def _write(path: Path, rendered: str) -> list[str]:
|
|
|
322
323
|
if path.exists() and not backup.exists():
|
|
323
324
|
shutil.copy2(path, backup)
|
|
324
325
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
325
|
-
path
|
|
326
|
+
write_text_keeping_newlines(path, rendered)
|
|
326
327
|
return [f"Wrote {path}"]
|
|
@@ -21,9 +21,11 @@ import re
|
|
|
21
21
|
import shutil
|
|
22
22
|
import subprocess
|
|
23
23
|
from dataclasses import dataclass
|
|
24
|
+
from datetime import datetime, timezone
|
|
24
25
|
from pathlib import Path
|
|
25
26
|
from typing import TYPE_CHECKING, Any
|
|
26
27
|
|
|
28
|
+
from memgraph_context_graph.adapters._text_file import write_text_keeping_newlines
|
|
27
29
|
from memgraph_context_graph.memory_backend import ExportReport, ImportReport, MemoryBackendError
|
|
28
30
|
|
|
29
31
|
if TYPE_CHECKING:
|
|
@@ -92,9 +94,16 @@ class ClaudeCodeMemory:
|
|
|
92
94
|
goes under its project's folder under its own filename instead, where
|
|
93
95
|
handing memory back finds it again. ``MEMORY.md`` is skipped: the index
|
|
94
96
|
is generated now. The original files are left as they are.
|
|
97
|
+
|
|
98
|
+
Each file is marked with the project it came from and when it last
|
|
99
|
+
changed there, which the session-start index ranks by. A file already
|
|
100
|
+
in the graph with the same text is marked too, so re-running the import
|
|
101
|
+
fills that in for an earlier one.
|
|
95
102
|
"""
|
|
96
103
|
from sessions_graph.memory_index import project_key_for
|
|
97
104
|
|
|
105
|
+
# An older sessions-graph can't record where a file came from; the import still works without it.
|
|
106
|
+
mark = getattr(files, "mark_imported", None)
|
|
98
107
|
report = ImportReport()
|
|
99
108
|
for memory_dir in sorted((home / ".claude" / "projects").glob("*/memory")):
|
|
100
109
|
checkout = _checkout_for(memory_dir.parent.name)
|
|
@@ -112,13 +121,15 @@ class ClaudeCodeMemory:
|
|
|
112
121
|
lineage = _lineage(files, target)
|
|
113
122
|
if lineage:
|
|
114
123
|
report.skipped.append(target)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
else:
|
|
125
|
+
try:
|
|
126
|
+
files.create(target, text)
|
|
127
|
+
except Exception: # Too large, or the path is taken by a directory: keep going.
|
|
128
|
+
report.skipped.append(target)
|
|
129
|
+
continue
|
|
130
|
+
report.imported.append(target)
|
|
131
|
+
if mark is not None and (not lineage or text in lineage):
|
|
132
|
+
mark(target, project, text=text, modified=_modified(source))
|
|
122
133
|
return report
|
|
123
134
|
|
|
124
135
|
def export_from(self, source: MemorySource, home: Path) -> ExportReport:
|
|
@@ -226,7 +237,7 @@ def _write_memory(folder: Path, name: str, text: str, lineage: set[str]) -> bool
|
|
|
226
237
|
if current == text or current not in lineage:
|
|
227
238
|
return False
|
|
228
239
|
folder.mkdir(parents=True, exist_ok=True)
|
|
229
|
-
path
|
|
240
|
+
write_text_keeping_newlines(path, text)
|
|
230
241
|
return True
|
|
231
242
|
|
|
232
243
|
|
|
@@ -240,7 +251,7 @@ def _write_index(folder: Path) -> None:
|
|
|
240
251
|
title = match["value"].strip("\"'") if (match := _NAME_RE.search(frontmatter)) else path.stem
|
|
241
252
|
hook = match["value"].strip("\"'") if (match := _DESCRIPTION_RE.search(frontmatter)) else ""
|
|
242
253
|
lines.append(f"- [{title}]({path.name})" + (f" — {hook}" if hook else ""))
|
|
243
|
-
(folder / _INDEX_FILE
|
|
254
|
+
write_text_keeping_newlines(folder / _INDEX_FILE, "\n".join(lines) + "\n")
|
|
244
255
|
|
|
245
256
|
|
|
246
257
|
def _frontmatter(text: str) -> str:
|
|
@@ -308,6 +319,11 @@ def _walk(directory: Path, tokens: list[str]) -> Path | None:
|
|
|
308
319
|
return None
|
|
309
320
|
|
|
310
321
|
|
|
322
|
+
def _modified(source: Path) -> str:
|
|
323
|
+
"""When *source* last changed, in the ISO 8601 UTC form the graph's ``updated_at`` uses."""
|
|
324
|
+
return datetime.fromtimestamp(source.stat().st_mtime, timezone.utc).isoformat()
|
|
325
|
+
|
|
326
|
+
|
|
311
327
|
def _fallback_key(slug: str) -> str:
|
|
312
328
|
"""A key for a project whose checkout is gone: the whole slug, since its last part alone may collide."""
|
|
313
329
|
from sessions_graph.memory_index import safe_key
|
|
@@ -344,7 +360,7 @@ def _set(path: Path, value: bool | None) -> list[str]:
|
|
|
344
360
|
if path.exists() and not backup.exists():
|
|
345
361
|
shutil.copy2(path, backup)
|
|
346
362
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
347
|
-
path
|
|
363
|
+
write_text_keeping_newlines(path, json.dumps(settings, indent=2) + "\n")
|
|
348
364
|
state = "removed" if value is None else json.dumps(value)
|
|
349
365
|
return [f"Wrote {path} ({_SETTING} {state})"]
|
|
350
366
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""OpenAI Codex command-hook runtime adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from memgraph_context_graph.adapters._spec import (
|
|
13
|
+
EventContext,
|
|
14
|
+
HookConfig,
|
|
15
|
+
RuntimeSpec,
|
|
16
|
+
SpecAdapter,
|
|
17
|
+
SpecPlugin,
|
|
18
|
+
hook_specific_session_context,
|
|
19
|
+
pre_tool_use_context,
|
|
20
|
+
session_end,
|
|
21
|
+
session_start,
|
|
22
|
+
tool_end,
|
|
23
|
+
tool_start,
|
|
24
|
+
turn_end,
|
|
25
|
+
write_hook_config,
|
|
26
|
+
)
|
|
27
|
+
from memgraph_context_graph.adapters._text_file import write_text_keeping_newlines
|
|
28
|
+
from memgraph_context_graph.adapters.codex_memory import CODEX_MEMORY
|
|
29
|
+
from memgraph_context_graph.events import MessageEvent, ToolEndEvent
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from memgraph_context_graph.events import Event
|
|
35
|
+
|
|
36
|
+
#: Enough of the rollout to hold the turn being handled, without reading a long session's whole file.
|
|
37
|
+
_ROLLOUT_TAIL_BYTES = 4 * 1024 * 1024
|
|
38
|
+
#: How long PostToolUse waits for Codex's rollout writer to record the call it is about.
|
|
39
|
+
_ITEM_WAIT_SECONDS = 0.5
|
|
40
|
+
_ITEM_POLL_SECONDS = 0.05
|
|
41
|
+
#: Rollout item types for the tools Codex's hooks report, and the ``tool_name`` the hooks give each.
|
|
42
|
+
_ITEM_TOOL_NAMES = {"CommandExecution": "Bash", "FileChange": "apply_patch"}
|
|
43
|
+
_TOOL_ITEM_TYPES = frozenset({*_ITEM_TOOL_NAMES, "McpToolCall"})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _user_prompt(context: EventContext) -> MessageEvent:
|
|
47
|
+
return MessageEvent(
|
|
48
|
+
**context.base(),
|
|
49
|
+
role="user",
|
|
50
|
+
content=context.value("prompt", ""),
|
|
51
|
+
model=context.optional_text("model"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _permission(context: EventContext) -> MessageEvent:
|
|
56
|
+
return MessageEvent(**context.base(), role="system", content=context.text("tool_name", "permission_request"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _completed_items(context: EventContext) -> dict[str, dict[str, Any]]:
|
|
60
|
+
"""This turn's ``item_completed`` records in Codex's rollout (``transcript_path``), by item id.
|
|
61
|
+
|
|
62
|
+
An item's id is the ``tool_use_id`` the tool hooks carry. Only the rollout's tail is read: the turn
|
|
63
|
+
being handled is the last one written. An unreadable rollout yields nothing.
|
|
64
|
+
"""
|
|
65
|
+
path, turn_id = context.optional_text("transcript_path"), context.optional_text("turn_id")
|
|
66
|
+
if not path or not turn_id:
|
|
67
|
+
return {}
|
|
68
|
+
try:
|
|
69
|
+
with open(path, "rb") as file:
|
|
70
|
+
start = max(0, file.seek(0, os.SEEK_END) - _ROLLOUT_TAIL_BYTES)
|
|
71
|
+
file.seek(start)
|
|
72
|
+
lines = file.read().split(b"\n")[1 if start else 0 :]
|
|
73
|
+
except OSError:
|
|
74
|
+
return {}
|
|
75
|
+
items = {}
|
|
76
|
+
for line in lines:
|
|
77
|
+
if b'"item_completed"' not in line:
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
record = json.loads(line)
|
|
81
|
+
except ValueError:
|
|
82
|
+
continue
|
|
83
|
+
payload = record.get("payload") if isinstance(record, dict) else None
|
|
84
|
+
if not isinstance(payload, dict) or payload.get("type") != "item_completed":
|
|
85
|
+
continue
|
|
86
|
+
item = payload.get("item")
|
|
87
|
+
if payload.get("turn_id") == turn_id and isinstance(item, dict) and isinstance(item.get("id"), str):
|
|
88
|
+
items[item["id"]] = item
|
|
89
|
+
return items
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _item_result(item: dict[str, Any]) -> Any:
|
|
93
|
+
"""What the tool behind a rollout item returned, shaped as its hook result would be."""
|
|
94
|
+
result = item.get("result")
|
|
95
|
+
if isinstance(result, dict):
|
|
96
|
+
return result.get("content", result)
|
|
97
|
+
return result if result is not None else item.get("aggregated_output", item.get("stdout"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _failure(item: dict[str, Any]) -> str | None:
|
|
101
|
+
"""The error message of a failed rollout item, or None if it succeeded."""
|
|
102
|
+
exit_code = item.get("exit_code")
|
|
103
|
+
if item.get("status") != "failed" and exit_code in (None, 0):
|
|
104
|
+
return None
|
|
105
|
+
blocks = _item_result(item)
|
|
106
|
+
texts = (
|
|
107
|
+
[b["text"] for b in blocks if isinstance(b, dict) and isinstance(b.get("text"), str)]
|
|
108
|
+
if isinstance(blocks, list)
|
|
109
|
+
else []
|
|
110
|
+
)
|
|
111
|
+
detail = "\n".join(texts) or item.get("error") or item.get("aggregated_output") or item.get("stderr")
|
|
112
|
+
if detail:
|
|
113
|
+
return str(detail)
|
|
114
|
+
return f"exit code {exit_code}" if exit_code not in (None, 0) else f"{item.get('type', 'tool')} failed"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _hook_tool_name(item: dict[str, Any]) -> str:
|
|
118
|
+
"""The ``tool_name`` Codex's hooks give the tool behind a rollout item."""
|
|
119
|
+
if item.get("type") == "McpToolCall":
|
|
120
|
+
server = re.sub(r"[^A-Za-z0-9_]", "_", str(item.get("server", "")))
|
|
121
|
+
return f"mcp__{server}__{item.get('tool', '')}"
|
|
122
|
+
return _ITEM_TOOL_NAMES[item["type"]]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _tool_end(context: EventContext) -> ToolEndEvent:
|
|
126
|
+
"""Codex's tool end, failed when the rollout says so.
|
|
127
|
+
|
|
128
|
+
A ``Bash`` call that exits non-zero still reaches PostToolUse, but the hook gets only its output;
|
|
129
|
+
the exit code is in the rollout, whose record of the call Codex normally writes before running the
|
|
130
|
+
hook (hence the short wait). A result stored as completed here stays so: :func:`_turn_end` can't
|
|
131
|
+
correct it. Other tools reach PostToolUse only when they succeeded.
|
|
132
|
+
"""
|
|
133
|
+
event = tool_end(context)
|
|
134
|
+
if event.tool_name != "Bash" or not event.tool_use_id:
|
|
135
|
+
return event
|
|
136
|
+
deadline = time.monotonic() + _ITEM_WAIT_SECONDS
|
|
137
|
+
while (item := _completed_items(context).get(event.tool_use_id)) is None and time.monotonic() < deadline:
|
|
138
|
+
time.sleep(_ITEM_POLL_SECONDS)
|
|
139
|
+
message = _failure(item) if item else None
|
|
140
|
+
return dataclasses.replace(event, is_error=True, error_message=message) if message else event
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _rollout_tool_end(context: EventContext, item_id: str, item: dict[str, Any]) -> ToolEndEvent:
|
|
144
|
+
message = _failure(item)
|
|
145
|
+
return ToolEndEvent(
|
|
146
|
+
**context.base(),
|
|
147
|
+
tool_name=_hook_tool_name(item),
|
|
148
|
+
tool_use_id=item_id,
|
|
149
|
+
result=_item_result(item),
|
|
150
|
+
is_error=message is not None,
|
|
151
|
+
error_message=message,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _turn_end(context: EventContext) -> list[Event]:
|
|
156
|
+
"""End the turn, first recording the outcome of every tool call that finished in it.
|
|
157
|
+
|
|
158
|
+
PostToolUse misses calls: Codex runs none for a failed tool (an MCP error, a rejected patch), nor for
|
|
159
|
+
a command that outlived its first yield and finished later. The turn's rollout records every one,
|
|
160
|
+
so each is recorded from there; one PostToolUse already recorded is the same event again, which
|
|
161
|
+
connectors store once. A patch Codex rejects before applying leaves no rollout item and stays a call
|
|
162
|
+
without a result.
|
|
163
|
+
"""
|
|
164
|
+
outcomes: list[Event] = [
|
|
165
|
+
_rollout_tool_end(context, item_id, item)
|
|
166
|
+
for item_id, item in _completed_items(context).items()
|
|
167
|
+
if item.get("type") in _TOOL_ITEM_TYPES
|
|
168
|
+
]
|
|
169
|
+
# Stop fires after every turn; SessionEnd, registered separately, ends the session.
|
|
170
|
+
return [*outcomes, *turn_end(context, reply=context.value("reply"))]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_HOOKS_PATH = ".codex/hooks.json"
|
|
174
|
+
SPEC = RuntimeSpec(
|
|
175
|
+
name="codex",
|
|
176
|
+
source_sdk="codex",
|
|
177
|
+
event_key="hook_event_name",
|
|
178
|
+
hooks=("SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PermissionRequest", "Stop", "SessionEnd"),
|
|
179
|
+
rules={
|
|
180
|
+
"SessionStart": session_start,
|
|
181
|
+
"UserPromptSubmit": _user_prompt,
|
|
182
|
+
"PreToolUse": tool_start,
|
|
183
|
+
"PostToolUse": _tool_end,
|
|
184
|
+
"PermissionRequest": _permission,
|
|
185
|
+
"Stop": _turn_end,
|
|
186
|
+
"SessionEnd": session_end,
|
|
187
|
+
},
|
|
188
|
+
metadata_keys=(
|
|
189
|
+
"cwd",
|
|
190
|
+
"source",
|
|
191
|
+
"transcript_path",
|
|
192
|
+
"turn_id",
|
|
193
|
+
"permission_mode",
|
|
194
|
+
"tool_name",
|
|
195
|
+
"tool_input",
|
|
196
|
+
"tool_use_id",
|
|
197
|
+
"reason",
|
|
198
|
+
"decision",
|
|
199
|
+
"stop_hook_active",
|
|
200
|
+
),
|
|
201
|
+
config=HookConfig(
|
|
202
|
+
path=_HOOKS_PATH,
|
|
203
|
+
layout="nested",
|
|
204
|
+
matchers={"SessionStart": "startup|resume|clear|compact", "PreToolUse": "*", "PostToolUse": "*"},
|
|
205
|
+
),
|
|
206
|
+
responses={"Stop": {"continue": True}},
|
|
207
|
+
context_before_tool=pre_tool_use_context,
|
|
208
|
+
session_context=hook_specific_session_context,
|
|
209
|
+
# Codex cuts hook context at about 2,500 tokens (docs/research/2026-10-codex-memory.md);
|
|
210
|
+
# 9,000 characters stays under that even with multi-byte text.
|
|
211
|
+
session_context_limit=9_000,
|
|
212
|
+
probe_payload={"hook_event_name": "Stop", "session_id": "doctor"},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class CodexHooksAdapter(SpecAdapter):
|
|
217
|
+
"""Convert Codex command-hook payloads into Event Protocol events."""
|
|
218
|
+
|
|
219
|
+
SPEC = SPEC
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def init(
|
|
223
|
+
project_dir: Path,
|
|
224
|
+
connectors: list[str],
|
|
225
|
+
*,
|
|
226
|
+
hook_command: str | None = None,
|
|
227
|
+
timeout: int = 30,
|
|
228
|
+
force: bool = False,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Generate private Codex ``config.toml`` and ``hooks.json`` files.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
FileExistsError: if either file exists and *force* is false.
|
|
234
|
+
"""
|
|
235
|
+
config_path = project_dir / ".codex" / "config.toml"
|
|
236
|
+
hooks_path = project_dir / _HOOKS_PATH
|
|
237
|
+
existing = [path for path in (config_path, hooks_path) if path.exists()]
|
|
238
|
+
if existing and not force:
|
|
239
|
+
names = ", ".join(str(path) for path in existing)
|
|
240
|
+
raise FileExistsError(f"Refusing to overwrite existing Codex config: {names} (pass force=True to replace)")
|
|
241
|
+
|
|
242
|
+
write_hook_config(SPEC, project_dir, connectors, hook_command=hook_command, timeout=timeout, force=True)
|
|
243
|
+
write_text_keeping_newlines(config_path, "[features]\nhooks = true\n")
|
|
244
|
+
print(f"Wrote {config_path}")
|
|
245
|
+
print(f"Wrote {hooks_path}")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
PLUGIN = SpecPlugin(SPEC, CodexHooksAdapter, init=init, native_memory=CODEX_MEMORY)
|
|
@@ -19,6 +19,7 @@ from memgraph_context_graph.adapters._spec import (
|
|
|
19
19
|
tool_start,
|
|
20
20
|
turn_end,
|
|
21
21
|
)
|
|
22
|
+
from memgraph_context_graph.adapters._text_file import write_text_keeping_newlines
|
|
22
23
|
from memgraph_context_graph.adapters.agent_memory import NO_BUILT_IN_MEMORY
|
|
23
24
|
from memgraph_context_graph.events import ErrorOccurredEvent, MessageEvent, ToolEndEvent
|
|
24
25
|
|
|
@@ -162,7 +163,7 @@ def init(
|
|
|
162
163
|
)
|
|
163
164
|
plugin = template.replace("__MEMGRAPH_CONTEXT_GRAPH_COMMAND__", json.dumps(argv))
|
|
164
165
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
-
target
|
|
166
|
+
write_text_keeping_newlines(target, plugin)
|
|
166
167
|
print(f"Wrote {target}")
|
|
167
168
|
|
|
168
169
|
|
{memgraph_context_graph-0.12.2 → memgraph_context_graph-0.12.4}/src/memgraph_context_graph/cli.py
RENAMED
|
@@ -311,7 +311,9 @@ def _join(argv: list[str]) -> int:
|
|
|
311
311
|
"""Point the config file at an Enterprise Memgraph as one login, then prove the login works."""
|
|
312
312
|
import getpass
|
|
313
313
|
|
|
314
|
-
|
|
314
|
+
import neo4j
|
|
315
|
+
|
|
316
|
+
from memgraph_context_graph.adapters._identity import MEMORY_BACKEND_GRAPH, config_file, reload_config, write_config
|
|
315
317
|
from memgraph_context_graph.spaces import EDITION_ENTERPRISE, SpaceError, spaces_for
|
|
316
318
|
|
|
317
319
|
parser = argparse.ArgumentParser(
|
|
@@ -340,9 +342,21 @@ def _join(argv: list[str]) -> int:
|
|
|
340
342
|
except (SpaceError, ValueError) as exc:
|
|
341
343
|
print(f"Wrote {config_file()}, but the login doesn't work yet: {exc}", file=sys.stderr)
|
|
342
344
|
return 1
|
|
345
|
+
except neo4j.exceptions.Neo4jError as exc:
|
|
346
|
+
refused = "Unauthenticated" in str(exc.code)
|
|
347
|
+
reason = f"Memgraph refused the login {args.login!r}; check its password" if refused else exc.message
|
|
348
|
+
print(f"Wrote {config_file()}, but the login doesn't work yet: {reason}", file=sys.stderr)
|
|
349
|
+
return 1
|
|
343
350
|
print(f"Wrote {config_file()}: edition = enterprise, login {args.login!r}.")
|
|
344
351
|
print(f"Personal space: {row['Current']}. Shared spaces: {', '.join(shared) or 'none yet'}.")
|
|
345
|
-
|
|
352
|
+
if reload_config().memory_backend == MEMORY_BACKEND_GRAPH:
|
|
353
|
+
print("Your agents' memory tool reaches them under /memories/spaces/<space>/.")
|
|
354
|
+
else:
|
|
355
|
+
# Shared spaces are only reachable through the memory tool, and that is on only with this backend (ADR 0006).
|
|
356
|
+
print(
|
|
357
|
+
"Next: memgraph-context-graph setup <runtime> (e.g. claude-code) --memory-backend context-graph\n"
|
|
358
|
+
" Shared spaces are reached through the memory tool, which only that backend turns on."
|
|
359
|
+
)
|
|
346
360
|
return 0
|
|
347
361
|
|
|
348
362
|
|
|
@@ -405,6 +419,7 @@ def doctor_checks(runtimes: Sequence[str], connectors: Sequence[str]) -> list[_C
|
|
|
405
419
|
# produce one, the config check already says why, and the rest would only repeat it.
|
|
406
420
|
if _edition_resolves():
|
|
407
421
|
checks.append(_check_memgraph())
|
|
422
|
+
checks.append(_check_graph_version(connectors))
|
|
408
423
|
for connector in connectors:
|
|
409
424
|
checks.append(_check_connector(connector))
|
|
410
425
|
if any(normalize_connector_name(connector) == "sessions-graph" for connector in connectors):
|
|
@@ -503,6 +518,19 @@ def _check_memgraph() -> _CheckResult:
|
|
|
503
518
|
_close(db)
|
|
504
519
|
|
|
505
520
|
|
|
521
|
+
def _check_graph_version(connectors: Sequence[str]) -> _CheckResult:
|
|
522
|
+
"""Whether a newer Context Graph has reshaped the graph past what this install can write or read."""
|
|
523
|
+
from memgraph_context_graph import graph_version
|
|
524
|
+
|
|
525
|
+
rules = graph_version.check_configured(fresh=True)
|
|
526
|
+
components = [graph_version.HUB, *(normalize_connector_name(connector) for connector in connectors)]
|
|
527
|
+
refused = [rules.refuse_write(c) for c in components if not rules.may_write(c)]
|
|
528
|
+
refused += [rules.refuse_read(c) for c in components if not rules.may_read(c)]
|
|
529
|
+
if refused:
|
|
530
|
+
return {"name": "graph version", "ok": False, "detail": "; ".join(dict.fromkeys(map(str, refused)))}
|
|
531
|
+
return {"name": "graph version", "ok": True, "detail": "this install can write and read the graph"}
|
|
532
|
+
|
|
533
|
+
|
|
506
534
|
def _close(db: Any) -> None:
|
|
507
535
|
if db is not None:
|
|
508
536
|
db.close()
|
|
@@ -60,7 +60,7 @@ class ConnectorSpec:
|
|
|
60
60
|
INSTALLABLE: dict[str, str] = {
|
|
61
61
|
"skills-graph": "skills-graph[memgraph-context-graph]>=0.4.0",
|
|
62
62
|
"actions-graph": "actions-graph[memgraph-context-graph]>=0.7.1",
|
|
63
|
-
"sessions-graph": "sessions-graph[memgraph-context-graph]>=0.12.
|
|
63
|
+
"sessions-graph": "sessions-graph[memgraph-context-graph]>=0.12.4",
|
|
64
64
|
"resources-graph": "resources-graph[memgraph-context-graph]>=0.3.0",
|
|
65
65
|
}
|
|
66
66
|
|