Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ cython_debug/
# Local agent hook configuration
/.codex/
/.claude/settings.local.json
/.private-specs/

# Project specific files
/enterprise-context/sic-agent/sic-scrapper/output/*
84 changes: 72 additions & 12 deletions context-graph/agent-context-graph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,78 @@ Planned:

Codex hook configuration is local environment wiring, so this repository ignores `.codex/`. Each developer should create their own local `.codex` files or use a user-level Codex config.

Prerequisites:

- Memgraph running and reachable over Bolt. Defaults are `bolt://localhost:7687`, empty user/password, and database `memgraph`.
- A Python environment that contains `agent-context-graph` and `skills-graph[agent-context-graph]`.
- Codex CLI or IDE extension running in a project that trusts the project-local `.codex/` layer.

The streamlined setup only needs two pieces of local information:

- where to write the Codex project config, usually your repo root
- where Memgraph is, plus optional auth/database values

If Memgraph is running locally with defaults:

```bash
agent-context-graph setup codex --project-dir "$PWD" --setup-schema
```

`--setup-schema` connects to Memgraph immediately and runs `SkillGraph().setup()`.

If you need non-default Memgraph connection values:

```bash
agent-context-graph setup codex \
--project-dir /path/to/your/repo \
--memgraph-url bolt://localhost:7687 \
--memgraph-user "" \
--memgraph-password "" \
--memgraph-database memgraph \
--setup-schema
```

The `--memgraph-*` options are used for `--setup-schema`, but they are not written into `.codex/hooks.json`.

For source development in this workspace:

```bash
uv run --package skills-graph --extra agent-context-graph \
python -m agent_context_graph.cli setup codex \
--project-dir /path/to/your/repo \
--memgraph-url bolt://localhost:7687 \
--setup-schema
```

The command writes local, ignored files:

```text
.codex/config.toml
.codex/hooks.json
```

It refuses to overwrite existing generated files unless you pass `--force`.

The generated hook command does not embed any Memgraph connection values. At runtime, Codex must run with the needed `MEMGRAPH_*` variables in its process environment, or the hooks will use `memgraph-toolbox` defaults.

If Memgraph requires a password, provide `MEMGRAPH_PASSWORD` to the Codex process environment. `.codex/hooks.json` should not contain Memgraph credentials.

Keep the Python environment used by the generated hook command around. Codex will run that absolute command path for every hook event.

To smoke test the generated command, copy the `"command"` value from `.codex/hooks.json` and run:

```bash
printf '{"hook_event_name":"Stop","session_id":"test"}' | COMMAND
```

The expected output is:

```json
{"continue": true}
```

If you prefer manual setup:

1. Make `skills-graph` able to reach Memgraph, then initialize and seed your skill graph once:

```bash
Expand Down Expand Up @@ -242,18 +314,6 @@ The resulting `.codex/hooks.json` has this shape:

The adapter records Codex `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest`, and `Stop` payloads. MCP tool names such as `mcp__skills__get_skill` are normalized by `skills-graph` to the underlying `get_skill` operation.

Smoke test the command:

```bash
printf '{"hook_event_name":"Stop","session_id":"test"}' | COMMAND
```

The expected output is:

```json
{"continue": true}
```

### Multiple Graph Components

```python
Expand Down
4 changes: 3 additions & 1 deletion context-graph/agent-context-graph/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = []
dependencies = [
"memgraph-toolbox",
]

[project.scripts]
agent-context-graph = "agent_context_graph.cli:main"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
_HELP = """usage: agent-context-graph <command> [options]

Commands:
setup <runtime> Configure an agent runtime.
hook <command> Configure or run command hooks.
"""

Expand All @@ -31,6 +32,11 @@ def main(argv: Sequence[str] | None = None) -> int:

return hook_main(args[1:])

if command == "setup":
from agent_context_graph.hooks.cli import main as hook_main

return hook_main(["init", *args[1:]])

print(f"Unknown command: {command}", file=sys.stderr)
print(_HELP)
return 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

import argparse
import json
import os
import shlex
import shutil
import sys
from pathlib import Path
from typing import TYPE_CHECKING

from memgraph_toolbox.api.memgraph import MEMGRAPH_ENV_KEYS, memgraph_env

if TYPE_CHECKING:
from collections.abc import Sequence

Expand Down Expand Up @@ -105,6 +108,31 @@ def _init_codex(argv: list[str]) -> int:
default=None,
help="Full command to place in hooks.json. Defaults to this installed CLI.",
)
parser.add_argument(
"--memgraph-url",
default=None,
help="Memgraph Bolt URL for the hook command. Defaults to MEMGRAPH_URL or bolt://localhost:7687.",
)
parser.add_argument(
"--memgraph-user",
default=None,
help="Memgraph username for the hook command. Defaults to MEMGRAPH_USER or empty.",
)
parser.add_argument(
"--memgraph-password",
default=None,
help="Memgraph password for --setup-schema. Never written to hooks.json.",
)
parser.add_argument(
"--memgraph-database",
default=None,
help="Memgraph database for the hook command. Defaults to MEMGRAPH_DATABASE or memgraph.",
)
parser.add_argument(
"--setup-schema",
action="store_true",
help="Connect to Memgraph now and initialize the skills-graph schema.",
)
parser.add_argument(
"--timeout",
type=int,
Expand Down Expand Up @@ -133,17 +161,22 @@ def _init_codex(argv: list[str]) -> int:

from agent_context_graph.adapters.codex import build_hooks_config

memgraph_values = _memgraph_env_from_args(args)
hook_command = args.hook_command or _default_hook_command("codex", connectors)
codex_dir.mkdir(parents=True, exist_ok=True)
config_path.write_text("[features]\ncodex_hooks = true\n", encoding="utf-8")
hooks_path.write_text(
json.dumps({"hooks": build_hooks_config(hook_command, timeout=args.timeout)}, indent=2) + "\n",
encoding="utf-8",
)
if args.setup_schema:
_setup_skills_graph_schema(memgraph_values)

print(f"Wrote {config_path}")
print(f"Wrote {hooks_path}")
print(f"Hook command: {hook_command}")
print(f"Memgraph URL: {memgraph_values['MEMGRAPH_URL']}")
print(f"Memgraph database: {memgraph_values['MEMGRAPH_DATABASE']}")
print(f"Hook command: {_mask_secret(hook_command, memgraph_values['MEMGRAPH_PASSWORD'])}")
return 0


Expand All @@ -156,5 +189,39 @@ def _default_hook_command(runtime: str, connectors: list[str]) -> str:
return shlex.join(args)


def _memgraph_env_from_args(args: argparse.Namespace) -> dict[str, str]:
return memgraph_env(
url=args.memgraph_url,
username=args.memgraph_user,
password=args.memgraph_password,
database=args.memgraph_database,
)


def _setup_skills_graph_schema(memgraph_env: dict[str, str]) -> None:
try:
from skills_graph import SkillGraph
except ImportError as exc:
msg = "skills-graph is required to initialize the skills-graph schema"
raise ImportError(msg) from exc

previous = {key: os.environ.get(key) for key in MEMGRAPH_ENV_KEYS}
os.environ.update(memgraph_env)
try:
SkillGraph().setup()
finally:
for key, value in previous.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value


def _mask_secret(value: str, secret: str) -> str:
if not secret:
return value
return value.replace(shlex.quote(secret), "'****'").replace(secret, "****")


if __name__ == "__main__":
raise SystemExit(main())
104 changes: 104 additions & 0 deletions context-graph/agent-context-graph/tests/test_hook_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import io
import json
import os
import sys
from types import ModuleType

from agent_context_graph.cli import main as top_level_main
from agent_context_graph.hooks.cli import main
Expand All @@ -23,6 +26,14 @@ def test_top_level_cli_dispatches_hook_run(monkeypatch, capsys):
assert capsys.readouterr().out.strip() == '{"continue": true}'


def test_top_level_cli_setup_aliases_codex_init(tmp_path, monkeypatch):
monkeypatch.setattr("agent_context_graph.hooks.cli.shutil.which", lambda _: "/bin/agent-context-graph")

assert top_level_main(["setup", "codex", "--project-dir", str(tmp_path)]) == 0

assert (tmp_path / ".codex" / "config.toml").read_text() == "[features]\ncodex_hooks = true\n"


def test_generic_cli_requires_runtime(capsys):
assert main([]) == 2

Expand Down Expand Up @@ -54,6 +65,99 @@ def test_init_codex_writes_private_config(tmp_path, capsys):
assert "Wrote" in capsys.readouterr().out


def test_init_codex_does_not_bake_memgraph_connection_into_hook_command(tmp_path, monkeypatch, capsys):
monkeypatch.setattr("agent_context_graph.hooks.cli.shutil.which", lambda _: "/bin/agent-context-graph")

assert (
main(
[
"init",
"codex",
"--project-dir",
str(tmp_path),
"--memgraph-url",
"bolt://memgraph.example:7687",
"--memgraph-user",
"neo",
"--memgraph-password",
"secret",
"--memgraph-database",
"skills",
]
)
== 0
)

hooks = json.loads((tmp_path / ".codex" / "hooks.json").read_text())
command = hooks["hooks"]["PreToolUse"][0]["hooks"][0]["command"]
assert command == "/bin/agent-context-graph hook run codex --connector skills-graph"
assert "MEMGRAPH_URL" not in command
assert "MEMGRAPH_USER" not in command
assert "MEMGRAPH_PASSWORD" not in command
assert "MEMGRAPH_DATABASE" not in command
assert "secret" not in capsys.readouterr().out


def test_init_codex_uses_memgraph_env_only_for_setup_schema(tmp_path, monkeypatch):
monkeypatch.setattr("agent_context_graph.hooks.cli.shutil.which", lambda _: "/bin/agent-context-graph")
captured = {}

class _SkillGraph:
def setup(self):
captured["url"] = os.environ.get("MEMGRAPH_URL")
captured["user"] = os.environ.get("MEMGRAPH_USER")
captured["password"] = os.environ.get("MEMGRAPH_PASSWORD")
captured["database"] = os.environ.get("MEMGRAPH_DATABASE")

fake_skills_graph = ModuleType("skills_graph")
fake_skills_graph.SkillGraph = _SkillGraph
monkeypatch.setitem(sys.modules, "skills_graph", fake_skills_graph)

assert (
main(
[
"init",
"codex",
"--project-dir",
str(tmp_path),
"--memgraph-url",
"bolt://memgraph.example:7687",
"--memgraph-user",
"neo",
"--memgraph-password",
"secret",
"--memgraph-database",
"skills",
"--setup-schema",
]
)
== 0
)

hooks = json.loads((tmp_path / ".codex" / "hooks.json").read_text())
command = hooks["hooks"]["PreToolUse"][0]["hooks"][0]["command"]
assert "MEMGRAPH" not in command
assert captured == {
"url": "bolt://memgraph.example:7687",
"user": "neo",
"password": "secret",
"database": "skills",
}


def test_init_codex_uses_memgraph_toolbox_env_helper(tmp_path, monkeypatch):
monkeypatch.setattr("agent_context_graph.hooks.cli.shutil.which", lambda _: "/bin/agent-context-graph")
monkeypatch.setenv("MEMGRAPH_URL", "bolt://env-memgraph:7687")
monkeypatch.setenv("MEMGRAPH_DATABASE", "env-skills")

assert main(["init", "codex", "--project-dir", str(tmp_path)]) == 0

hooks = json.loads((tmp_path / ".codex" / "hooks.json").read_text())
command = hooks["hooks"]["PreToolUse"][0]["hooks"][0]["command"]
assert "MEMGRAPH_URL" not in command
assert "MEMGRAPH_DATABASE" not in command


def test_init_codex_refuses_to_overwrite_without_force(tmp_path, capsys):
codex_dir = tmp_path / ".codex"
codex_dir.mkdir()
Expand Down
Loading
Loading