memgraph-context-graph 0.12.0__tar.gz → 0.12.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.
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/PKG-INFO +1 -1
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/memory.md +53 -19
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/research/2026-10-codex-memory.md +2 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/pyproject.toml +1 -1
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/_spec.py +33 -5
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/antigravity_cli.py +3 -1
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/claude_code_memory.py +113 -39
- memgraph_context_graph-0.12.2/src/memgraph_context_graph/adapters/codex_memory.py +396 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/connectors.py +1 -1
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/runner.py +83 -7
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/memory_backend.py +37 -7
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/engine.py +19 -8
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/tool.py +2 -2
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_agent_memory_wiring.py +7 -1
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_antigravity_cli_adapter.py +16 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_capture_log.py +43 -0
- memgraph_context_graph-0.12.2/tests/test_claude_code_memory.py +31 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_codex_adapter.py +13 -1
- memgraph_context_graph-0.12.2/tests/test_codex_memory.py +69 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_setup.py +1 -1
- memgraph_context_graph-0.12.0/src/memgraph_context_graph/adapters/codex_memory.py +0 -241
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/.gitignore +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/CONTEXT.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/LICENSE +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/README.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0001-runtime-adapter-terminology.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0002-config-file-only-hook-resolution.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0003-config-path-override.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0004-user-id-in-session-start-event.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0005-config-file-covers-llm-key-and-subprocess-env.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0006-memory-tool-writes-explicit-memory.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/adr/0007-spawned-processes-read-the-config-file-themselves.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/command-hooks.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/configuration.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/extending.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/runtime-setup.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/docs/sdk-memory.md +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/examples/sdk_claude_agent.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/examples/sdk_openai_agents.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/__init__.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/__init__.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/_identity.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/_opencode_plugin.js +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/agent_mcp.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/agent_memory.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/claude_code.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/codex.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/copilot_cli.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/cursor.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/grok.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/adapters/opencode.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/capture_log.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/capture_policy.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/cli.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/config_schema.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/events.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/forget.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/__init__.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/cli.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/hooks/runtime_plugin.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/link.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/mcp_server.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/protocols.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/sdk/__init__.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/sdk/claude.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/sdk/openai.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/session.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/__init__.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/agents.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/cli.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/memgraph.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/prompts.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/uninstall.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/setup/update.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/spaces.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/terminal.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/src/memgraph_context_graph/tools.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/__init__.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/conftest.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/fake_agent_cli.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_capture_policy.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_claude_code_adapter.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_config_cli.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_connectors.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_context_before_tool.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_copilot_cli_adapter.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_cursor_adapter.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_dev_scripts.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_events.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_grok_adapter.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_hook_cli.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_identity.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_link.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_opencode_adapter.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_plugin_hooks.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_runtime_plugin.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_session.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_setup_docker.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_spaces.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_terminal.py +0 -0
- {memgraph_context_graph-0.12.0 → memgraph_context_graph-0.12.2}/tests/test_tools.py +0 -0
|
@@ -89,7 +89,9 @@ back on, since without the plugin it would have none.)
|
|
|
89
89
|
|
|
90
90
|
Opting in does three things, in this order:
|
|
91
91
|
1. Imports the existing auto-memory files from every project once:
|
|
92
|
-
- `user`/`feedback` memories go to `/memories/<type
|
|
92
|
+
- `user`/`feedback` memories go to `/memories/<type>/`. When another
|
|
93
|
+
project already put a different memory of the same name there, this one
|
|
94
|
+
goes to its own project's folder instead, so neither is lost;
|
|
93
95
|
- `project`/`reference` memories go to that project's folder;
|
|
94
96
|
- `MEMORY.md` is skipped, and anything already in the graph is never overwritten.
|
|
95
97
|
2. Sets `autoMemoryEnabled: false` in `~/.claude/settings.json`, the user-level
|
|
@@ -100,12 +102,20 @@ Opting in does three things, in this order:
|
|
|
100
102
|
For Codex, `setup codex --memory-backend context-graph` does the same against
|
|
101
103
|
its global store, with three differences:
|
|
102
104
|
- **Import.** `memory_summary.md`, the profile Codex injected each session,
|
|
103
|
-
becomes `/memories/
|
|
104
|
-
`/memories/codex/memory.md`.
|
|
105
|
+
becomes `/memories/codex/memory-summary.md`, and `MEMORY.md` becomes
|
|
106
|
+
`/memories/codex/memory.md`. Both stay under `/memories/codex/` rather than
|
|
107
|
+
the user-wide root, since Codex wrote them across every project.
|
|
105
108
|
- **Switch.** It turns off `features.memories`, `memories.use_memories` and
|
|
106
109
|
`memories.generate_memories` in `~/.codex/config.toml` (or under
|
|
107
|
-
`$CODEX_HOME`), keeping the user's comments and
|
|
108
|
-
|
|
110
|
+
`$CODEX_HOME`), keeping the user's comments, layout and line endings. The
|
|
111
|
+
file is first copied to `config.toml.context-graph-memory.bak`, apart from
|
|
112
|
+
the backup setup's own writes leave. Handing memory back restores those keys
|
|
113
|
+
from it, drops a `[memories]` table the switch created, and deletes the
|
|
114
|
+
backup. Repeating the hand-back preserves the restored settings. If the
|
|
115
|
+
switch backup is missing, including on older installs, current settings stay
|
|
116
|
+
unchanged. The general `config.toml.context-graph.bak` may predate the switch
|
|
117
|
+
and is never used to guess the prior settings; the output explains how to
|
|
118
|
+
turn Codex memories on explicitly when needed. If the user's own
|
|
109
119
|
`[mcp_servers.context-graph]` narrows tools with `enabled_tools`, `memory` is
|
|
110
120
|
added there.
|
|
111
121
|
- **Compaction.** The Codex plugin's `SessionStart` hook also fires after
|
|
@@ -123,20 +133,44 @@ Nothing writes to both memories: while the backend is `context-graph`, the
|
|
|
123
133
|
graph is the only one. Handing memory back (`--memory-backend native`, or
|
|
124
134
|
`uninstall`) first copies the graph's memories into Claude Code's auto memory,
|
|
125
135
|
the reverse of the import, so it picks up where the graph left off:
|
|
126
|
-
-
|
|
127
|
-
|
|
128
|
-
`<type>_` prefix back;
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
- Claude Code keeps one memory folder per git repository, shared by its
|
|
137
|
+
worktrees and subdirectories, so a project's files go to that repository's
|
|
138
|
+
folder only, each with its `<type>_` prefix back;
|
|
139
|
+
- `user`/`feedback` files go back to the folders they were imported from. One
|
|
140
|
+
first written while the graph was the backend goes to the repositories of
|
|
141
|
+
the sessions that wrote it. Claude Code has no memory that applies
|
|
142
|
+
everywhere, so these files are not copied into every project;
|
|
143
|
+
- a file already there is replaced only when it is an earlier text of the
|
|
144
|
+
graph's file. Any other file, changed outside the graph, is kept, and the
|
|
145
|
+
output lists it. Each folder that changed gets its `MEMORY.md` rebuilt;
|
|
146
|
+
- files with no folder to go to (a project Claude Code never opened, a
|
|
147
|
+
user-wide file no recorded session wrote, another agent's files) stay in
|
|
148
|
+
the graph only, and the output lists them.
|
|
149
|
+
|
|
150
|
+
Then it removes the setting. If Memgraph is down, `setup` switches nothing;
|
|
151
|
+
`uninstall` turns the memory back on anyway, with what it held before, and
|
|
152
|
+
warns. Copilot CLI and Grok Build get nothing copied back: their memory is
|
|
153
|
+
hosted by the agent, or kept in folders only it can name.
|
|
154
|
+
|
|
155
|
+
Codex rewrites its own memory files at each consolidation, so handing back
|
|
156
|
+
leaves each graph memory as an ad-hoc note in
|
|
157
|
+
`~/.codex/memories/extensions/ad_hoc/notes/` (`memories_v2/` under
|
|
158
|
+
`[memories] version = "v2"`), the one place Codex takes outside writes. Its
|
|
159
|
+
next consolidation folds them into `memory_summary.md` and `MEMORY.md`, in its
|
|
160
|
+
own words, so they aren't copied exactly. That runs only while
|
|
161
|
+
`features.memories` and `memories.generate_memories` are on; the output says
|
|
162
|
+
when they weren't before the switch. A note names its project when the memory
|
|
163
|
+
had one, since Codex keeps one store for all projects. Files the import took
|
|
164
|
+
from Codex and the graph never changed aren't noted, and a note already left
|
|
165
|
+
isn't left again.
|
|
166
|
+
|
|
167
|
+
The backend is one setting for every agent, and the copy runs only while it
|
|
168
|
+
is `context-graph`. So `setup codex claude-code --memory-backend native` hands
|
|
169
|
+
each named agent back first and changes the setting last. Setup stops handing
|
|
170
|
+
back agents at the first failure and keeps the graph backend, so retrying the
|
|
171
|
+
command can still export their memories. An agent left out
|
|
172
|
+
of that command gets nothing copied back once the setting is `native`, so name
|
|
173
|
+
every agent that uses the graph's memory.
|
|
140
174
|
|
|
141
175
|
`doctor` fails when the backend is `context-graph` but Claude Code's own memory
|
|
142
176
|
is still on, including when the project's `.claude/settings.json` or
|
|
@@ -80,6 +80,8 @@ The `[memories]` table (`MemoriesToml`, `config/src/types.rs:322`):
|
|
|
80
80
|
- `rollout_summaries/<timestamp>-<shorthash>[-<slug>].md`: one recap per selected rollout. Each recap points at the original session `rollout_path`, which is a jsonl file under `~/.codex/sessions/`.
|
|
81
81
|
- `skills/<name>/SKILL.md` (with optional `scripts/`, `examples/`, `templates/`): skills the consolidator promotes.
|
|
82
82
|
- `extensions/ad_hoc/notes/<timestamp>-<slug>.md`: the only place the model may write. It does so only when the user explicitly asks to update memory, and Phase 2 folds the note in later.
|
|
83
|
+
- The name must be `YYYY-MM-DDTHH-MM-SS-<slug>.md`, with a slug of 1 to 80 lowercase letters, digits and hyphens, and 128 bytes at most. A note is created, never overwritten (`ext/memories/src/local/ad_hoc_note.rs`). Phase 2 is told to treat notes as information, never instructions, to never delete one, and to tag what it derives from them `[ad-hoc note]` (`memories/write/templates/extensions/ad_hoc/instructions.md`). [src]
|
|
84
|
+
- **Live check, codex-cli 0.162.0, 2026-10-27.** Two notes were written into a throwaway `CODEX_HOME` with memories on. One session's Phase 2 folded both into `memory_summary.md` and `MEMORY.md`, tagged `[ad-hoc note]`. That included a note the first baseline had already committed. The consolidation agent runs in the background of the session that claims the job, and it is shut down with that session: a `codex exec` that ends in seconds kills it (`failed_agent`), and the job then backs off for an hour. [local]
|
|
83
85
|
- `phase2_workspace_diff.md` (transient) and `.git/`: the git baseline that Phase 2 diffs against.
|
|
84
86
|
|
|
85
87
|
**Injection** [src `ext/memories/src/prompts.rs`, `extension.rs`, `core/src/session/mod.rs`]:
|
|
@@ -9,8 +9,10 @@ only declares what differs about it.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
|
+
import os
|
|
12
13
|
import shlex
|
|
13
14
|
import shutil
|
|
15
|
+
import subprocess
|
|
14
16
|
import sys
|
|
15
17
|
from collections.abc import Callable, Mapping
|
|
16
18
|
from dataclasses import dataclass, field
|
|
@@ -69,6 +71,9 @@ class FieldMap:
|
|
|
69
71
|
reply: tuple[str, ...] = ("last_assistant_message",)
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
_ON_WINDOWS = os.name == "nt"
|
|
75
|
+
|
|
76
|
+
|
|
72
77
|
@dataclass(frozen=True)
|
|
73
78
|
class HookConfig:
|
|
74
79
|
"""Describe how a runtime represents and stores command hooks.
|
|
@@ -78,6 +83,9 @@ class HookConfig:
|
|
|
78
83
|
``flat_hooks`` names events written with the flat layout even when
|
|
79
84
|
``layout`` is nested, for runtimes that only accept matcher groups on some
|
|
80
85
|
events.
|
|
86
|
+
``windows_shell`` is the shell the runtime hands hook commands to on
|
|
87
|
+
Windows. ``cmd.exe`` doesn't read POSIX single quotes, so a ``"cmd"``
|
|
88
|
+
runtime's commands are quoted for it instead.
|
|
81
89
|
"""
|
|
82
90
|
|
|
83
91
|
layout: Literal["nested", "flat"]
|
|
@@ -89,6 +97,7 @@ class HookConfig:
|
|
|
89
97
|
inject_event_name: bool = False
|
|
90
98
|
matchers: Mapping[str, str] = field(default_factory=dict)
|
|
91
99
|
flat_hooks: frozenset[str] = frozenset()
|
|
100
|
+
windows_shell: Literal["posix", "cmd"] = "posix"
|
|
92
101
|
|
|
93
102
|
|
|
94
103
|
@dataclass(frozen=True)
|
|
@@ -300,7 +309,7 @@ def build_hooks_config(spec: RuntimeSpec, command: str, *, timeout: int = 30) ->
|
|
|
300
309
|
for hook_name in spec.hooks:
|
|
301
310
|
hook_command = command
|
|
302
311
|
if spec.config.inject_event_name:
|
|
303
|
-
hook_command = f"{command} --event-name {
|
|
312
|
+
hook_command = f"{command} --event-name {_quote(hook_name, spec.config.windows_shell)}"
|
|
304
313
|
command_entry: dict[str, Any] = {"command": hook_command, "type": "command"}
|
|
305
314
|
if spec.config.timeout_key is not None:
|
|
306
315
|
command_entry[spec.config.timeout_key] = timeout
|
|
@@ -343,7 +352,11 @@ def write_hook_config(
|
|
|
343
352
|
if config_path.exists() and not spec.config.merge and not force:
|
|
344
353
|
raise FileExistsError(f"Refusing to overwrite existing {spec.name} config: {config_path} (pass force=True)")
|
|
345
354
|
|
|
346
|
-
command = hook_command or hook_command_for(spec.name, connectors)
|
|
355
|
+
command = hook_command or hook_command_for(spec.name, connectors, windows_shell=spec.config.windows_shell)
|
|
356
|
+
if hook_command is None and timeout != 30:
|
|
357
|
+
from memgraph_context_graph.hooks.runner import deadline_for
|
|
358
|
+
|
|
359
|
+
command = f"{command} --deadline {deadline_for(timeout):g}"
|
|
347
360
|
hooks = build_hooks_config(spec, command, timeout=timeout)
|
|
348
361
|
document: dict[str, Any]
|
|
349
362
|
if config_path.exists() and spec.config.merge:
|
|
@@ -444,9 +457,24 @@ def hook_command_argv(runtime: str, connectors: list[str]) -> list[str]:
|
|
|
444
457
|
return argv
|
|
445
458
|
|
|
446
459
|
|
|
447
|
-
def hook_command_for(runtime: str, connectors: list[str]) -> str:
|
|
448
|
-
"""Build the installed hook command for *runtime* and *connectors* as a shell string.
|
|
449
|
-
|
|
460
|
+
def hook_command_for(runtime: str, connectors: list[str], *, windows_shell: Literal["posix", "cmd"] = "posix") -> str:
|
|
461
|
+
"""Build the installed hook command for *runtime* and *connectors* as a shell string.
|
|
462
|
+
|
|
463
|
+
On Windows a ``"cmd"`` *windows_shell* gets ``cmd.exe`` quoting, which
|
|
464
|
+
leaves a path without spaces bare; elsewhere the command is POSIX-quoted.
|
|
465
|
+
"""
|
|
466
|
+
argv = hook_command_argv(runtime, connectors)
|
|
467
|
+
if _uses_cmd(windows_shell):
|
|
468
|
+
return subprocess.list2cmdline(argv)
|
|
469
|
+
return shlex.join(argv)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _uses_cmd(windows_shell: Literal["posix", "cmd"]) -> bool:
|
|
473
|
+
return windows_shell == "cmd" and _ON_WINDOWS
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _quote(word: str, windows_shell: Literal["posix", "cmd"]) -> str:
|
|
477
|
+
return subprocess.list2cmdline([word]) if _uses_cmd(windows_shell) else shlex.quote(word)
|
|
450
478
|
|
|
451
479
|
|
|
452
480
|
def _metadata_from_payload(payload: dict[str, Any], keys: tuple[str, ...]) -> dict[str, Any]:
|
|
@@ -183,7 +183,8 @@ SPEC = RuntimeSpec(
|
|
|
183
183
|
),
|
|
184
184
|
# hooks.json is keyed by named hook groups; this runtime owns one group.
|
|
185
185
|
# Only tool events take matcher groups: agy rejects the whole file if a
|
|
186
|
-
# lifecycle event is nested, so those are written as flat handlers.
|
|
186
|
+
# lifecycle event is nested, so those are written as flat handlers. On
|
|
187
|
+
# Windows agy runs each command through cmd.exe, inside its own quotes.
|
|
187
188
|
config=HookConfig(
|
|
188
189
|
path=".agents/hooks.json",
|
|
189
190
|
layout="nested",
|
|
@@ -191,6 +192,7 @@ SPEC = RuntimeSpec(
|
|
|
191
192
|
merge=True,
|
|
192
193
|
inject_event_name=True,
|
|
193
194
|
flat_hooks=frozenset({"SessionStart", "Stop"}),
|
|
195
|
+
windows_shell="cmd",
|
|
194
196
|
),
|
|
195
197
|
fields=FieldMap(session_id=("conversationId",)),
|
|
196
198
|
# PostToolUse requires an object; an empty PreToolUse answer leaves agy's
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Claude Code's auto memory, as Context Graph switches away from and imports it.
|
|
2
2
|
|
|
3
3
|
Auto memory is Markdown files in ``~/.claude/projects/<project>/memory/``, one
|
|
4
|
-
folder per
|
|
5
|
-
|
|
4
|
+
folder per git repository, shared by its worktrees and subdirectories (outside
|
|
5
|
+
a repository, per project folder). ``<project>`` is that folder's absolute path
|
|
6
|
+
with every character other than a letter or digit replaced by ``-``; the other
|
|
7
|
+
``~/.claude/projects/`` folders, one per directory a session started in, hold
|
|
8
|
+
transcripts and are never read for memory. ``MEMORY.md`` there
|
|
6
9
|
is the index Claude Code loads at session start; the other files are
|
|
7
10
|
memories, each with frontmatter naming its ``type``. The ``autoMemoryEnabled``
|
|
8
11
|
setting turns it off; a plugin can't set it, so this edits the user's own
|
|
@@ -16,6 +19,7 @@ from __future__ import annotations
|
|
|
16
19
|
import json
|
|
17
20
|
import re
|
|
18
21
|
import shutil
|
|
22
|
+
import subprocess
|
|
19
23
|
from dataclasses import dataclass
|
|
20
24
|
from pathlib import Path
|
|
21
25
|
from typing import TYPE_CHECKING, Any
|
|
@@ -23,7 +27,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
23
27
|
from memgraph_context_graph.memory_backend import ExportReport, ImportReport, MemoryBackendError
|
|
24
28
|
|
|
25
29
|
if TYPE_CHECKING:
|
|
26
|
-
from memgraph_context_graph.memory_backend import MemoryFiles, MemorySource
|
|
30
|
+
from memgraph_context_graph.memory_backend import MemoryFiles, MemorySource
|
|
27
31
|
|
|
28
32
|
_SETTING = "autoMemoryEnabled"
|
|
29
33
|
_INDEX_FILE = "MEMORY.md"
|
|
@@ -83,8 +87,11 @@ class ClaudeCodeMemory:
|
|
|
83
87
|
``user``/``feedback`` memories go under ``/memories/<type>/``, since they
|
|
84
88
|
apply everywhere; ``project``/``reference`` (and untyped) ones under
|
|
85
89
|
that project's ``/memories/projects/<key>/``, keyed as the session-start
|
|
86
|
-
index keys the checkout. ``
|
|
87
|
-
|
|
90
|
+
index keys the checkout. A ``user``/``feedback`` memory whose name the
|
|
91
|
+
graph already holds with other text, so another project's own version,
|
|
92
|
+
goes under its project's folder under its own filename instead, where
|
|
93
|
+
handing memory back finds it again. ``MEMORY.md`` is skipped: the index
|
|
94
|
+
is generated now. The original files are left as they are.
|
|
88
95
|
"""
|
|
89
96
|
from sessions_graph.memory_index import project_key_for
|
|
90
97
|
|
|
@@ -99,7 +106,11 @@ class ClaudeCodeMemory:
|
|
|
99
106
|
if not text.strip():
|
|
100
107
|
continue
|
|
101
108
|
target = _target_path(source.name, text, project)
|
|
102
|
-
|
|
109
|
+
lineage = _lineage(files, target)
|
|
110
|
+
if lineage and text not in lineage and not target.startswith("/memories/projects/"):
|
|
111
|
+
target = f"/memories/projects/{project}/{source.name}"
|
|
112
|
+
lineage = _lineage(files, target)
|
|
113
|
+
if lineage:
|
|
103
114
|
report.skipped.append(target)
|
|
104
115
|
continue
|
|
105
116
|
try:
|
|
@@ -113,48 +124,87 @@ class ClaudeCodeMemory:
|
|
|
113
124
|
def export_from(self, source: MemorySource, home: Path) -> ExportReport:
|
|
114
125
|
"""Write the graph's memories back into auto memory, the reverse of :meth:`import_into`.
|
|
115
126
|
|
|
116
|
-
A project's files go to
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
A project's files go to its repository's memory folder. A ``user``/``feedback`` file goes back
|
|
128
|
+
to the folders it was imported from, which still hold it; one first written in the graph goes
|
|
129
|
+
to the repositories of the sessions that wrote it. A file there is overwritten only when it is
|
|
130
|
+
an earlier text of the graph's file, so a newer or unrelated native file is kept and reported.
|
|
131
|
+
Each folder written to gets its ``MEMORY.md`` rebuilt. Files with no folder to go to (a project
|
|
132
|
+
Claude Code never opened, nested paths, other agents' files) stay in the graph only.
|
|
121
133
|
"""
|
|
122
134
|
folders = _memory_folders(home)
|
|
135
|
+
every_folder = sorted({folder for found in folders.values() for folder in found})
|
|
123
136
|
report = ExportReport()
|
|
124
|
-
|
|
137
|
+
written: set[Path] = set()
|
|
125
138
|
for memory in source.files():
|
|
126
139
|
parts = memory.path.removeprefix("/memories/").split("/")
|
|
127
140
|
if len(parts) == 2 and parts[0] in _USER_WIDE_TYPES:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
name = _native_name(parts[1], memory.content, parts[0])
|
|
142
|
+
targets = [folder for folder in every_folder if (folder / name).is_file()] or [
|
|
143
|
+
folder for key in source.written_in(memory.path) for folder in folders.get(key, [])
|
|
144
|
+
]
|
|
145
|
+
elif len(parts) == 3 and parts[0] == "projects":
|
|
146
|
+
name = _native_name(parts[2], memory.content, "project")
|
|
147
|
+
targets = folders.get(parts[1], [])
|
|
133
148
|
else:
|
|
149
|
+
targets = []
|
|
150
|
+
if not targets:
|
|
134
151
|
report.skipped.append(memory.path)
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
continue
|
|
153
|
+
lineage = {memory.content} | {version.content for version in source.versions(memory.path)}
|
|
154
|
+
kept = []
|
|
137
155
|
for folder in targets:
|
|
138
|
-
_write_memory(folder, name, memory.content)
|
|
139
|
-
|
|
140
|
-
|
|
156
|
+
if _write_memory(folder, name, memory.content, lineage):
|
|
157
|
+
written.add(folder)
|
|
158
|
+
elif (folder / name).read_text(encoding="utf-8") != memory.content:
|
|
159
|
+
kept.append(str(folder / name))
|
|
160
|
+
report.kept += kept
|
|
161
|
+
if len(kept) < len(targets):
|
|
162
|
+
report.exported.append(memory.path)
|
|
163
|
+
for folder in sorted(written):
|
|
141
164
|
_write_index(folder)
|
|
142
165
|
return report
|
|
143
166
|
|
|
144
167
|
|
|
145
168
|
def _memory_folders(home: Path) -> dict[str, list[Path]]:
|
|
146
|
-
"""Each project key's auto-memory folders
|
|
169
|
+
"""Each project key's auto-memory folders, keyed as the import keys them: one per repository Claude Code opened.
|
|
170
|
+
|
|
171
|
+
Every ``~/.claude/projects/`` folder whose checkout is in a repository leads to the one folder named after
|
|
172
|
+
that repository's main checkout, the only one Claude Code reads memory from; a folder whose checkout is
|
|
173
|
+
gone counts only when it already holds memory.
|
|
174
|
+
"""
|
|
147
175
|
from sessions_graph.memory_index import project_key_for
|
|
148
176
|
|
|
149
177
|
projects = home / ".claude" / "projects"
|
|
150
|
-
folders: dict[str,
|
|
178
|
+
folders: dict[str, set[Path]] = {}
|
|
151
179
|
for project in sorted(projects.iterdir() if projects.is_dir() else []):
|
|
152
180
|
if not project.is_dir():
|
|
153
181
|
continue
|
|
154
182
|
checkout = _checkout_for(project.name)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
183
|
+
if checkout is None:
|
|
184
|
+
if (project / "memory").is_dir():
|
|
185
|
+
folders.setdefault(_fallback_key(project.name), set()).add(project / "memory")
|
|
186
|
+
continue
|
|
187
|
+
root = _repository_root(checkout) or checkout
|
|
188
|
+
folder = projects / _SLUG_RE.sub("-", str(root)) / "memory"
|
|
189
|
+
key = project_key_for(checkout) or _fallback_key(folder.parent.name)
|
|
190
|
+
folders.setdefault(key, set()).add(folder)
|
|
191
|
+
return {key: sorted(found) for key, found in folders.items()}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _repository_root(checkout: Path) -> Path | None:
|
|
195
|
+
"""The main checkout of the repository *checkout* is in, which its worktrees share; None outside git."""
|
|
196
|
+
try:
|
|
197
|
+
result = subprocess.run(
|
|
198
|
+
["git", "-C", str(checkout), "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
199
|
+
capture_output=True,
|
|
200
|
+
text=True,
|
|
201
|
+
timeout=5,
|
|
202
|
+
check=False,
|
|
203
|
+
)
|
|
204
|
+
except (OSError, subprocess.SubprocessError):
|
|
205
|
+
return None
|
|
206
|
+
common = Path(result.stdout.strip())
|
|
207
|
+
return common.parent if result.returncode == 0 and common.name == ".git" else None
|
|
158
208
|
|
|
159
209
|
|
|
160
210
|
def _native_name(name: str, text: str, default_type: str) -> str:
|
|
@@ -164,12 +214,20 @@ def _native_name(name: str, text: str, default_type: str) -> str:
|
|
|
164
214
|
return name if _PREFIX_RE.match(name) else f"{memory_type}_{name}"
|
|
165
215
|
|
|
166
216
|
|
|
167
|
-
def _write_memory(folder: Path, name: str, text: str) ->
|
|
217
|
+
def _write_memory(folder: Path, name: str, text: str, lineage: set[str]) -> bool:
|
|
218
|
+
"""Write *text* to *folder*/*name* unless a file there holds text outside *lineage*; returns whether it wrote.
|
|
219
|
+
|
|
220
|
+
*lineage* is the graph file's current and earlier texts. A native file among them is one the graph's copy
|
|
221
|
+
descends from, so writing over it loses nothing; any other was written beside the graph and is kept.
|
|
222
|
+
"""
|
|
168
223
|
path = folder / name
|
|
169
|
-
if path.is_file()
|
|
170
|
-
|
|
224
|
+
if path.is_file():
|
|
225
|
+
current = path.read_text(encoding="utf-8")
|
|
226
|
+
if current == text or current not in lineage:
|
|
227
|
+
return False
|
|
171
228
|
folder.mkdir(parents=True, exist_ok=True)
|
|
172
229
|
path.write_text(text, encoding="utf-8")
|
|
230
|
+
return True
|
|
173
231
|
|
|
174
232
|
|
|
175
233
|
def _write_index(folder: Path) -> None:
|
|
@@ -198,23 +256,39 @@ def _target_path(filename: str, text: str, project: str) -> str:
|
|
|
198
256
|
return f"/memories/projects/{project}/{name}"
|
|
199
257
|
|
|
200
258
|
|
|
201
|
-
def
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return
|
|
206
|
-
return
|
|
259
|
+
def _lineage(files: MemoryFiles, path: str) -> set[str]:
|
|
260
|
+
"""The graph file at *path*'s current and earlier texts; empty when there is no such file."""
|
|
261
|
+
current = files.read(path)
|
|
262
|
+
if current is None:
|
|
263
|
+
return set()
|
|
264
|
+
return {current} | {version.content for version in files.versions(path)}
|
|
207
265
|
|
|
208
266
|
|
|
209
267
|
def _checkout_for(slug: str) -> Path | None:
|
|
210
|
-
"""The directory Claude Code named *slug*, found by walking the filesystem from
|
|
268
|
+
"""The directory Claude Code named *slug*, found by walking the filesystem from its root.
|
|
211
269
|
|
|
212
270
|
The slug is lossy: ``/repos/ai-toolkit`` and ``/repos/ai/toolkit`` give
|
|
213
271
|
the same one. So each step matches the directory's real entries by their
|
|
214
272
|
own slugs, preferring the longest, and backtracks when a path dead-ends.
|
|
215
273
|
"""
|
|
216
|
-
|
|
217
|
-
return _walk(
|
|
274
|
+
start = _slug_root(slug)
|
|
275
|
+
return _walk(*start) if start else None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _slug_root(slug: str) -> tuple[Path, list[str]] | None:
|
|
279
|
+
"""The filesystem root *slug* starts from, and the tokens left to walk below it.
|
|
280
|
+
|
|
281
|
+
A POSIX slug starts with the ``-`` of ``/``. A Windows one starts with the
|
|
282
|
+
drive letter, and the drive's ``:\\`` makes a double dash: ``C:\\source\\x`` is
|
|
283
|
+
``C--source-x``.
|
|
284
|
+
"""
|
|
285
|
+
if slug.startswith("-"):
|
|
286
|
+
tokens = slug[1:].split("-")
|
|
287
|
+
return (Path("/"), tokens) if tokens[0] else None
|
|
288
|
+
drive, double_dash, rest = slug.partition("--")
|
|
289
|
+
if len(drive) == 1 and drive.isalpha() and double_dash and rest:
|
|
290
|
+
return Path(f"{drive}:/"), rest.split("-")
|
|
291
|
+
return None
|
|
218
292
|
|
|
219
293
|
|
|
220
294
|
def _walk(directory: Path, tokens: list[str]) -> Path | None:
|