memplex 3.2.1__tar.gz → 3.2.3__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.
- {memplex-3.2.1 → memplex-3.2.3}/PKG-INFO +1 -1
- {memplex-3.2.1 → memplex-3.2.3}/README.md +52 -10
- {memplex-3.2.1 → memplex-3.2.3}/memplex/__init__.py +1 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/__main__.py +1 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/_plugin/.claude-plugin/plugin.json +1 -1
- memplex-3.2.3/memplex/_plugin/hooks/hooks.json +81 -0
- memplex-3.2.3/memplex/_plugin/scripts/hook-runner.py +418 -0
- memplex-3.2.3/memplex/adapters/agent_installer.py +952 -0
- memplex-3.2.3/memplex/adapters/agent_runtime.py +399 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/adapters/cli.py +227 -122
- {memplex-3.2.1 → memplex-3.2.3}/memplex/adapters/http_api.py +14 -6
- {memplex-3.2.1 → memplex-3.2.3}/memplex/adapters/mcp_server.py +133 -11
- memplex-3.2.3/memplex/benchmarks/__init__.py +83 -0
- memplex-3.2.3/memplex/benchmarks/base.py +268 -0
- memplex-3.2.3/memplex/benchmarks/benchmark_cli.py +208 -0
- memplex-3.2.3/memplex/benchmarks/evaluator.py +552 -0
- memplex-3.2.3/memplex/benchmarks/loader.py +342 -0
- memplex-3.2.3/memplex/benchmarks/locomo.py +677 -0
- memplex-3.2.3/memplex/benchmarks/memory_eval.py +756 -0
- memplex-3.2.3/memplex/benchmarks/memory_metrics.py +151 -0
- memplex-3.2.3/memplex/benchmarks/metrics.py +551 -0
- memplex-3.2.3/memplex/benchmarks/nq_trivia.py +1073 -0
- memplex-3.2.3/memplex/benchmarks/popqa_hotpot.py +958 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/compaction.py +21 -46
- {memplex-3.2.1 → memplex-3.2.3}/memplex/config.py +14 -9
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/associator/domain_classifier.py +87 -9
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/associator/entity_aligner.py +17 -22
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/associator/ref_linker.py +64 -35
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/associator/term_mapper.py +7 -8
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/dictionaries/__init__.py +3 -2
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/engine.py +107 -74
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/extractors/docx.py +8 -3
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/extractors/image.py +13 -7
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/extractors/markdown.py +69 -26
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/extractors/pdf.py +33 -20
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/extractors/vision_mapper.py +14 -15
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/handlers/clipboard.py +5 -12
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/handlers/file_handler.py +19 -12
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/handlers/url_handler.py +24 -19
- memplex-3.2.3/memplex/core/hooks/__init__.py +11 -0
- memplex-3.2.3/memplex/core/hooks/collector.py +241 -0
- memplex-3.2.3/memplex/core/hooks/hook_event.py +38 -0
- memplex-3.2.3/memplex/core/hooks/registry.py +128 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/enhancer.py +3 -9
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/fallback_chain.py +9 -3
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/injection_guard.py +5 -5
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/providers/__init__.py +2 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/providers/anthropic.py +2 -3
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/providers/local.py +5 -4
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/providers/rule_based.py +30 -8
- memplex-3.2.3/memplex/logging_utils.py +270 -0
- memplex-3.2.3/memplex/metrics.py +382 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/models/memory.py +11 -4
- {memplex-3.2.1 → memplex-3.2.3}/memplex/models/misc.py +9 -5
- {memplex-3.2.1 → memplex-3.2.3}/memplex/models/search.py +2 -2
- {memplex-3.2.1 → memplex-3.2.3}/memplex/models/task.py +2 -2
- {memplex-3.2.1 → memplex-3.2.3}/memplex/processing/graph_builder.py +39 -35
- {memplex-3.2.1 → memplex-3.2.3}/memplex/processing/merger/__init__.py +1 -1
- {memplex-3.2.1 → memplex-3.2.3}/memplex/processing/merger/confidence_calculator.py +7 -10
- {memplex-3.2.1 → memplex-3.2.3}/memplex/processing/merger/conflict_resolver.py +17 -9
- {memplex-3.2.1 → memplex-3.2.3}/memplex/retrieval/dedup.py +9 -18
- {memplex-3.2.1 → memplex-3.2.3}/memplex/retrieval/embedding.py +10 -22
- {memplex-3.2.1 → memplex-3.2.3}/memplex/retrieval/reranker.py +4 -9
- {memplex-3.2.1 → memplex-3.2.3}/memplex/service.py +294 -75
- {memplex-3.2.1 → memplex-3.2.3}/memplex/storage/__init__.py +15 -16
- {memplex-3.2.1 → memplex-3.2.3}/memplex/storage/changelog.py +1 -3
- {memplex-3.2.1 → memplex-3.2.3}/memplex/storage/feedback.py +46 -31
- {memplex-3.2.1 → memplex-3.2.3}/memplex/storage/lite/store.py +80 -86
- {memplex-3.2.1 → memplex-3.2.3}/memplex/storage/vector.py +12 -10
- {memplex-3.2.1 → memplex-3.2.3}/memplex/wiki/community.py +24 -24
- {memplex-3.2.1 → memplex-3.2.3}/memplex/wiki/compiler.py +41 -50
- {memplex-3.2.1 → memplex-3.2.3}/memplex/wiki/generator.py +17 -13
- {memplex-3.2.1 → memplex-3.2.3}/memplex/wiki/search.py +38 -28
- {memplex-3.2.1 → memplex-3.2.3}/memplex/worker.py +38 -23
- {memplex-3.2.1 → memplex-3.2.3}/memplex.egg-info/PKG-INFO +1 -1
- {memplex-3.2.1 → memplex-3.2.3}/memplex.egg-info/SOURCES.txt +22 -0
- {memplex-3.2.1 → memplex-3.2.3}/pyproject.toml +36 -1
- memplex-3.2.3/tests/test_agent_hot_paths.py +286 -0
- memplex-3.2.3/tests/test_agent_runtime.py +199 -0
- {memplex-3.2.1 → memplex-3.2.3}/tests/test_associators.py +3 -5
- {memplex-3.2.1 → memplex-3.2.3}/tests/test_config.py +10 -11
- {memplex-3.2.1 → memplex-3.2.3}/tests/test_core_engine.py +9 -9
- {memplex-3.2.1 → memplex-3.2.3}/tests/test_graph_builder.py +52 -23
- memplex-3.2.3/tests/test_hooks.py +1335 -0
- memplex-3.2.3/tests/test_install_scripts.py +239 -0
- {memplex-3.2.1 → memplex-3.2.3}/tests/test_llm.py +35 -36
- {memplex-3.2.1 → memplex-3.2.3}/tests/test_models.py +13 -12
- {memplex-3.2.1 → memplex-3.2.3}/tests/test_service.py +18 -14
- {memplex-3.2.1 → memplex-3.2.3}/tests/test_storage.py +73 -38
- memplex-3.2.1/memplex/_plugin/hooks/hooks.json +0 -43
- memplex-3.2.1/memplex/_plugin/scripts/hook-runner.py +0 -166
- memplex-3.2.1/tests/test_hooks.py +0 -453
- {memplex-3.2.1 → memplex-3.2.3}/LICENSE +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/_plugin/.mcp.json +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/_plugin/__init__.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/_plugin/skills/mem-explore/SKILL.md +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/_plugin/skills/mem-manage/SKILL.md +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/_plugin/skills/mem-search/SKILL.md +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/_plugin/skills/mem-write/SKILL.md +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/adapters/__init__.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/adapters/claude_skill.py +1 -1
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/__init__.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/associator/__init__.py +3 -3
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/extractors/__init__.py +2 -2
- {memplex-3.2.1 → memplex-3.2.3}/memplex/core/handlers/__init__.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/__init__.py +3 -3
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/provider.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/llm/sanitizer.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/models/__init__.py +46 -46
- {memplex-3.2.1 → memplex-3.2.3}/memplex/models/feedback.py +1 -1
- {memplex-3.2.1 → memplex-3.2.3}/memplex/models/graph.py +1 -1
- {memplex-3.2.1 → memplex-3.2.3}/memplex/models/paragraph.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/models/source.py +1 -1
- {memplex-3.2.1 → memplex-3.2.3}/memplex/processing/__init__.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/retrieval/__init__.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/storage/base.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/storage/lite/__init__.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex/wiki/__init__.py +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex.egg-info/dependency_links.txt +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex.egg-info/entry_points.txt +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex.egg-info/requires.txt +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/memplex.egg-info/top_level.txt +0 -0
- {memplex-3.2.1 → memplex-3.2.3}/setup.cfg +0 -0
|
@@ -13,24 +13,59 @@
|
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
-
###
|
|
16
|
+
### One-Command Agent Setup
|
|
17
|
+
|
|
18
|
+
No source checkout is required. The npm entrypoint matches the common
|
|
19
|
+
`npx <tool> setup` pattern used by modern CLIs:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx memplex setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Install into a specific local agent:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx memplex setup --agent codex --project-path "$PWD"
|
|
29
|
+
npx memplex setup --agent claude-code --project-path "$PWD"
|
|
30
|
+
npx memplex setup --agent openclaw --project-path "$PWD"
|
|
31
|
+
npx memplex setup --agent hermes --project-path "$PWD"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install every supported agent config on this machine:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx memplex setup --agent all --project-path "$PWD"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Uninstall:
|
|
17
41
|
|
|
18
|
-
**Option 1: Local plugin (for development)**
|
|
19
42
|
```bash
|
|
20
|
-
|
|
21
|
-
cd plugin
|
|
22
|
-
/plugin install ./plugin
|
|
43
|
+
npx memplex uninstall --agent all
|
|
23
44
|
```
|
|
24
45
|
|
|
25
|
-
|
|
46
|
+
The npm wrapper creates a persistent Python environment at
|
|
47
|
+
`~/.local/share/memplex/agent-venv`, installs `memplex==3.2.3`, detects local
|
|
48
|
+
Codex, Claude Code, OpenClaw, and Hermes config directories/commands, then
|
|
49
|
+
registers Memplex with each detected agent. It uses `uv` when available and
|
|
50
|
+
falls back to `python -m venv` plus `pip`.
|
|
51
|
+
|
|
52
|
+
Python-first users can use a persistent tool install:
|
|
53
|
+
|
|
26
54
|
```bash
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
uv tool install memplex==3.2.3
|
|
56
|
+
memplex setup
|
|
29
57
|
```
|
|
30
58
|
|
|
31
|
-
|
|
59
|
+
The raw hosted installer remains available for shell-only environments:
|
|
60
|
+
|
|
32
61
|
```bash
|
|
33
|
-
|
|
62
|
+
curl -fsSL https://raw.githubusercontent.com/articultur/memplex/main/scripts/install-agent.sh | bash
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Claude Code Plugin
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
memplex setup --agent claude-code
|
|
34
69
|
```
|
|
35
70
|
|
|
36
71
|
### From Source
|
|
@@ -41,6 +76,13 @@ cd memplex
|
|
|
41
76
|
pip install -e .
|
|
42
77
|
```
|
|
43
78
|
|
|
79
|
+
Uninstall:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
curl -fsSL https://raw.githubusercontent.com/articultur/memplex/main/scripts/install-agent.sh | \
|
|
83
|
+
bash -s -- --agent hermes --uninstall
|
|
84
|
+
```
|
|
85
|
+
|
|
44
86
|
## Claude Code Setup
|
|
45
87
|
|
|
46
88
|
After installation, initialize Memplex:
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Memplex memory system hooks",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"Setup": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "*",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"shell": "bash",
|
|
11
|
+
"command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${MEMPLEX_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; ls -dt \"$_C/plugins/cache/articultur/memplex\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/articultur/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/hook-runner.py\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"memplex: hook-runner.py not found\" >&2; exit 1; }; _PY=\"${MEMPLEX_PYTHON:-$(command -v python3 || command -v python || true)}\"; [ -n \"$_PY\" ] || { echo \"memplex: python not found\" >&2; exit 1; }; \"$_PY\" \"$_P/scripts/hook-runner.py\" setup",
|
|
12
|
+
"timeout": 300
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"SessionStart": [
|
|
18
|
+
{
|
|
19
|
+
"matcher": "startup|resume|clear|compact",
|
|
20
|
+
"hooks": [
|
|
21
|
+
{
|
|
22
|
+
"type": "command",
|
|
23
|
+
"shell": "bash",
|
|
24
|
+
"command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${MEMPLEX_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; ls -dt \"$_C/plugins/cache/articultur/memplex\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/articultur/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/hook-runner.py\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"memplex: hook-runner.py not found\" >&2; exit 1; }; _PY=\"${MEMPLEX_PYTHON:-$(command -v python3 || command -v python || true)}\"; [ -n \"$_PY\" ] || { echo \"memplex: python not found\" >&2; exit 1; }; \"$_PY\" \"$_P/scripts/hook-runner.py\" session-start",
|
|
25
|
+
"timeout": 60
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"UserPromptSubmit": [
|
|
31
|
+
{
|
|
32
|
+
"hooks": [
|
|
33
|
+
{
|
|
34
|
+
"type": "command",
|
|
35
|
+
"shell": "bash",
|
|
36
|
+
"command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${MEMPLEX_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; ls -dt \"$_C/plugins/cache/articultur/memplex\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/articultur/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/hook-runner.py\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"memplex: hook-runner.py not found\" >&2; exit 1; }; _PY=\"${MEMPLEX_PYTHON:-$(command -v python3 || command -v python || true)}\"; [ -n \"$_PY\" ] || { echo \"memplex: python not found\" >&2; exit 1; }; \"$_PY\" \"$_P/scripts/hook-runner.py\" prompt-submit",
|
|
37
|
+
"timeout": 30
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"PreToolUse": [
|
|
43
|
+
{
|
|
44
|
+
"matcher": "*",
|
|
45
|
+
"hooks": [
|
|
46
|
+
{
|
|
47
|
+
"type": "command",
|
|
48
|
+
"shell": "bash",
|
|
49
|
+
"command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${MEMPLEX_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; ls -dt \"$_C/plugins/cache/articultur/memplex\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/articultur/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/hook-runner.py\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"memplex: hook-runner.py not found\" >&2; exit 1; }; _PY=\"${MEMPLEX_PYTHON:-$(command -v python3 || command -v python || true)}\"; [ -n \"$_PY\" ] || { echo \"memplex: python not found\" >&2; exit 1; }; \"$_PY\" \"$_P/scripts/hook-runner.py\" file-context",
|
|
50
|
+
"timeout": 60
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"PostToolUse": [
|
|
56
|
+
{
|
|
57
|
+
"matcher": "*",
|
|
58
|
+
"hooks": [
|
|
59
|
+
{
|
|
60
|
+
"type": "command",
|
|
61
|
+
"shell": "bash",
|
|
62
|
+
"command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${MEMPLEX_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; ls -dt \"$_C/plugins/cache/articultur/memplex\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/articultur/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/hook-runner.py\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"memplex: hook-runner.py not found\" >&2; exit 1; }; _PY=\"${MEMPLEX_PYTHON:-$(command -v python3 || command -v python || true)}\"; [ -n \"$_PY\" ] || { echo \"memplex: python not found\" >&2; exit 1; }; \"$_PY\" \"$_P/scripts/hook-runner.py\" observation \"$MEMPLEX_TOOL_NAME\" \"$MEMPLEX_SESSION_ID\"",
|
|
63
|
+
"timeout": 120
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"Stop": [
|
|
69
|
+
{
|
|
70
|
+
"hooks": [
|
|
71
|
+
{
|
|
72
|
+
"type": "command",
|
|
73
|
+
"shell": "bash",
|
|
74
|
+
"command": "export PATH=\"$($SHELL -lc 'echo $PATH' 2>/dev/null):$PATH\"; _C=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"; _E=\"${MEMPLEX_PLUGIN_ROOT:-${PLUGIN_ROOT:-}}\"; _P=$({ [ -n \"$_E\" ] && printf '%s\\n' \"$_E\"; ls -dt \"$_C/plugins/cache/articultur/memplex\"/[0-9]*/ 2>/dev/null; printf '%s\\n' \"$_C/plugins/marketplaces/articultur/plugin\"; } | while IFS= read -r _R; do _R=\"${_R%/}\"; [ -d \"$_R/plugin/scripts\" ] && _Q=\"$_R/plugin\" || _Q=\"$_R\"; [ -f \"$_Q/scripts/hook-runner.py\" ] && { printf '%s\\n' \"$_Q\"; break; }; done); [ -n \"$_P\" ] || { echo \"memplex: hook-runner.py not found\" >&2; exit 1; }; _PY=\"${MEMPLEX_PYTHON:-$(command -v python3 || command -v python || true)}\"; [ -n \"$_PY\" ] || { echo \"memplex: python not found\" >&2; exit 1; }; \"$_PY\" \"$_P/scripts/hook-runner.py\" summarize",
|
|
75
|
+
"timeout": 120
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Memplex Hook Runner -- dispatches lifecycle hooks for Claude Code.
|
|
3
|
+
|
|
4
|
+
Called by plugin/hooks/hooks.json with subcommands:
|
|
5
|
+
setup - Environment check on plugin install
|
|
6
|
+
session-start - Load project context on session start
|
|
7
|
+
prompt-submit - Inject relevant memories on user prompt
|
|
8
|
+
file-context - PreToolUse context for Read operations
|
|
9
|
+
observation - Auto-collect observation from tool usage
|
|
10
|
+
summarize - Session summary and compaction
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
python hook-runner.py setup
|
|
14
|
+
python hook-runner.py session-start
|
|
15
|
+
python hook-runner.py prompt-submit
|
|
16
|
+
python hook-runner.py file-context
|
|
17
|
+
python hook-runner.py observation <tool_name> <session_id>
|
|
18
|
+
python hook-runner.py summarize
|
|
19
|
+
|
|
20
|
+
Output contract (Claude Code hook):
|
|
21
|
+
{"continue":true,"suppressOutput":true} - Non-blocking, no output shown
|
|
22
|
+
stdout content - Injected as context
|
|
23
|
+
exit 0 - Success
|
|
24
|
+
exit 1 - Non-blocking error
|
|
25
|
+
exit 2 - Blocking error
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
import sys
|
|
35
|
+
import time
|
|
36
|
+
import warnings
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any, Optional
|
|
39
|
+
|
|
40
|
+
# Suppress noisy logging from dependencies
|
|
41
|
+
logging.getLogger("sentence_transformers").setLevel(logging.ERROR)
|
|
42
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
43
|
+
warnings.filterwarnings("ignore", message=".*unauthenticated requests.*")
|
|
44
|
+
|
|
45
|
+
# Output contract prefix for Claude Code hooks
|
|
46
|
+
OUTPUT_CONTRACT = '{"continue":true,"suppressOutput":true}'
|
|
47
|
+
|
|
48
|
+
# Rate limiting
|
|
49
|
+
_RATE_FILE = Path("/tmp/.memplex_last_obs")
|
|
50
|
+
_RATE_LIMIT_SECONDS = 30
|
|
51
|
+
|
|
52
|
+
# Private tag stripping
|
|
53
|
+
_PRIVATE_TAG_RE = re.compile(r"<private>.*?</private>", re.DOTALL)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _strip_private_tags(text: str) -> str:
|
|
57
|
+
return _PRIVATE_TAG_RE.sub("", text)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _find_plugin_root() -> Optional[Path]:
|
|
61
|
+
"""Find the plugin root using Claude Code's convention.
|
|
62
|
+
|
|
63
|
+
Searches in order:
|
|
64
|
+
1. MEMPLEX_PLUGIN_ROOT env var
|
|
65
|
+
2. PLUGIN_ROOT env var
|
|
66
|
+
3. Claude plugin cache: ~/.claude/plugins/cache/articultur/memplex/<version>/
|
|
67
|
+
4. Claude marketplace: ~/.claude/plugins/marketplaces/articultur/plugin/
|
|
68
|
+
"""
|
|
69
|
+
# Check env vars first
|
|
70
|
+
for env_var in ("MEMPLEX_PLUGIN_ROOT", "PLUGIN_ROOT"):
|
|
71
|
+
root = os.environ.get(env_var, "")
|
|
72
|
+
if root:
|
|
73
|
+
p = Path(root)
|
|
74
|
+
if p.exists():
|
|
75
|
+
return p
|
|
76
|
+
|
|
77
|
+
# Search standard locations
|
|
78
|
+
claude_config = os.environ.get("CLAUDE_CONFIG_DIR", Path.home() / ".claude")
|
|
79
|
+
|
|
80
|
+
# Try cache versions
|
|
81
|
+
cache_base = Path(claude_config) / "plugins" / "cache" / "articultur" / "memplex"
|
|
82
|
+
if cache_base.exists():
|
|
83
|
+
# Get latest version directory
|
|
84
|
+
versions = []
|
|
85
|
+
for d in cache_base.iterdir():
|
|
86
|
+
if d.is_dir():
|
|
87
|
+
versions.append(d)
|
|
88
|
+
if versions:
|
|
89
|
+
# Sort by name (version directories)
|
|
90
|
+
versions.sort(key=lambda x: x.name, reverse=True)
|
|
91
|
+
for v in versions:
|
|
92
|
+
scripts_dir = v / "scripts"
|
|
93
|
+
if scripts_dir.exists():
|
|
94
|
+
hook_script = scripts_dir / "hook-runner.py"
|
|
95
|
+
if hook_script.exists():
|
|
96
|
+
return scripts_dir.parent
|
|
97
|
+
|
|
98
|
+
# Try marketplace
|
|
99
|
+
marketplace = Path(claude_config) / "plugins" / "marketplaces" / "articultur" / "plugin"
|
|
100
|
+
if marketplace.exists():
|
|
101
|
+
scripts_dir = marketplace / "scripts"
|
|
102
|
+
if scripts_dir.exists() and (scripts_dir / "hook-runner.py").exists():
|
|
103
|
+
return marketplace
|
|
104
|
+
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _ensure_memplex_importable() -> None:
|
|
109
|
+
"""Ensure memplex package is importable."""
|
|
110
|
+
if "memplex" in sys.modules:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Try to find the project root (parent of plugin directory)
|
|
114
|
+
plugin_root = _find_plugin_root()
|
|
115
|
+
if plugin_root:
|
|
116
|
+
project_root = plugin_root.parent.parent
|
|
117
|
+
if str(project_root) not in sys.path:
|
|
118
|
+
sys.path.insert(0, str(project_root))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _init_service():
|
|
122
|
+
"""Initialize MemplexService."""
|
|
123
|
+
_ensure_memplex_importable()
|
|
124
|
+
from memplex.config import load_config
|
|
125
|
+
from memplex.service import MemplexService
|
|
126
|
+
|
|
127
|
+
cfg = load_config()
|
|
128
|
+
return MemplexService(config=cfg)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _project_path() -> str:
|
|
132
|
+
return os.environ.get("MEMPLEX_PROJECT_ROOT") or os.getcwd()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _session_id(default: str = "claude-code") -> str:
|
|
136
|
+
return os.environ.get("MEMPLEX_SESSION_ID") or default
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _user_id() -> str:
|
|
140
|
+
return os.environ.get("MEMPLEX_USER_ID") or os.environ.get("USER") or "default"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _init_runtime(session_id: str = ""):
|
|
144
|
+
"""Initialize the shared agent runtime for Claude Code hooks."""
|
|
145
|
+
_ensure_memplex_importable()
|
|
146
|
+
from memplex.adapters.agent_runtime import AgentMemoryRuntime
|
|
147
|
+
|
|
148
|
+
return AgentMemoryRuntime(
|
|
149
|
+
agent="claude-code",
|
|
150
|
+
user_id=_user_id(),
|
|
151
|
+
session_id=session_id or _session_id(),
|
|
152
|
+
project_path=_project_path(),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _rate_file() -> Path:
|
|
157
|
+
return Path(os.environ.get("MEMPLEX_OBS_RATE_FILE", str(_RATE_FILE)))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _read_stdin_json() -> dict[str, Any]:
|
|
161
|
+
try:
|
|
162
|
+
if sys.stdin.isatty():
|
|
163
|
+
return {}
|
|
164
|
+
raw = sys.stdin.read()
|
|
165
|
+
if not raw.strip():
|
|
166
|
+
return {}
|
|
167
|
+
data = json.loads(raw)
|
|
168
|
+
return data if isinstance(data, dict) else {}
|
|
169
|
+
except (json.JSONDecodeError, OSError):
|
|
170
|
+
return {}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _hook_payload(data: dict[str, Any]) -> dict[str, Any]:
|
|
174
|
+
payload = data.get("tool_input", data)
|
|
175
|
+
return payload if isinstance(payload, dict) else {}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _first_text(data: dict[str, Any], *keys: str) -> str:
|
|
179
|
+
for key in keys:
|
|
180
|
+
value = data.get(key)
|
|
181
|
+
if isinstance(value, str) and value.strip():
|
|
182
|
+
return value
|
|
183
|
+
return ""
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _tool_name(data: dict[str, Any], fallback: str = "") -> str:
|
|
187
|
+
return (
|
|
188
|
+
fallback
|
|
189
|
+
or os.environ.get("MEMPLEX_TOOL_NAME")
|
|
190
|
+
or _first_text(data, "tool_name", "toolName", "name")
|
|
191
|
+
or "unknown"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _print_contract(content: str = "") -> None:
|
|
196
|
+
"""Print output with Claude Code contract."""
|
|
197
|
+
if content:
|
|
198
|
+
print(content)
|
|
199
|
+
print(OUTPUT_CONTRACT)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_setup() -> None:
|
|
203
|
+
"""Check environment on plugin install."""
|
|
204
|
+
try:
|
|
205
|
+
_ensure_memplex_importable()
|
|
206
|
+
import memplex
|
|
207
|
+
from memplex.config import load_config
|
|
208
|
+
|
|
209
|
+
# Verify memplex is importable
|
|
210
|
+
version = getattr(memplex, "__version__", "unknown")
|
|
211
|
+
|
|
212
|
+
# Check config
|
|
213
|
+
try:
|
|
214
|
+
cfg = load_config()
|
|
215
|
+
except Exception:
|
|
216
|
+
# Config doesn't exist yet, that's okay for setup
|
|
217
|
+
cfg = None
|
|
218
|
+
|
|
219
|
+
# Initialize service if config exists
|
|
220
|
+
if cfg:
|
|
221
|
+
from memplex.service import MemplexService
|
|
222
|
+
service = MemplexService(config=cfg)
|
|
223
|
+
health = service.health()
|
|
224
|
+
print(f"[Memplex] v{version} installed. Status: {health.get('status', 'unknown')}")
|
|
225
|
+
else:
|
|
226
|
+
print(f"[Memplex] v{version} installed. Run 'memplex config init' to configure.")
|
|
227
|
+
|
|
228
|
+
except ImportError as e:
|
|
229
|
+
print(f"[Memplex] Setup failed: {e}", file=sys.stderr)
|
|
230
|
+
print("[Memplex] Install memplex: pip install memplex", file=sys.stderr)
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print(f"[Memplex] Setup warning: {e}", file=sys.stderr)
|
|
234
|
+
# Non-fatal, exit 0
|
|
235
|
+
|
|
236
|
+
_print_contract()
|
|
237
|
+
sys.exit(0)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def cmd_session_start() -> None:
|
|
241
|
+
"""Load project context and inject relevant memories."""
|
|
242
|
+
try:
|
|
243
|
+
runtime = _init_runtime()
|
|
244
|
+
recalled = runtime.before_prompt(f"session start {_project_path()}")
|
|
245
|
+
if recalled.context:
|
|
246
|
+
_print_contract("[Memplex Context]\n" + recalled.context)
|
|
247
|
+
else:
|
|
248
|
+
_print_contract("[Memplex] No memories yet for this project.")
|
|
249
|
+
except Exception as e:
|
|
250
|
+
print(f"[Memplex] session-start: {e}", file=sys.stderr)
|
|
251
|
+
_print_contract()
|
|
252
|
+
sys.exit(0)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def cmd_prompt_submit() -> None:
|
|
256
|
+
"""Inject relevant memories based on user prompt context.
|
|
257
|
+
|
|
258
|
+
This hook runs on every user prompt submission. It reads the prompt
|
|
259
|
+
from stdin and queries for relevant memories to inject.
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
data = _read_stdin_json()
|
|
263
|
+
prompt = _first_text(data, "text", "prompt", "message", "user_prompt")
|
|
264
|
+
|
|
265
|
+
if not prompt:
|
|
266
|
+
_print_contract()
|
|
267
|
+
sys.exit(0)
|
|
268
|
+
|
|
269
|
+
recalled = _init_runtime().before_prompt(prompt)
|
|
270
|
+
if recalled.context:
|
|
271
|
+
_print_contract("[Memplex] Related memories:\n" + recalled.context)
|
|
272
|
+
else:
|
|
273
|
+
_print_contract()
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
print(f"[Memplex] prompt-submit: {e}", file=sys.stderr)
|
|
277
|
+
_print_contract()
|
|
278
|
+
sys.exit(0)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def cmd_file_context() -> None:
|
|
282
|
+
"""PreToolUse context for Read operations.
|
|
283
|
+
|
|
284
|
+
When Claude Code is about to read files, this hook can inject
|
|
285
|
+
relevant memories about those files or their content.
|
|
286
|
+
"""
|
|
287
|
+
try:
|
|
288
|
+
data = _read_stdin_json()
|
|
289
|
+
payload = _hook_payload(data)
|
|
290
|
+
file_path = _first_text(payload, "file_path", "path")
|
|
291
|
+
|
|
292
|
+
if not file_path:
|
|
293
|
+
_print_contract()
|
|
294
|
+
sys.exit(0)
|
|
295
|
+
|
|
296
|
+
filename = Path(file_path).name
|
|
297
|
+
runtime = _init_runtime()
|
|
298
|
+
recalled = runtime.before_prompt(filename)
|
|
299
|
+
if not recalled.context:
|
|
300
|
+
recalled = runtime.before_prompt(f"file {filename} {file_path}")
|
|
301
|
+
if recalled.context:
|
|
302
|
+
_print_contract("[Memplex] Related to this file:\n" + recalled.context)
|
|
303
|
+
else:
|
|
304
|
+
_print_contract()
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
print(f"[Memplex] file-context: {e}", file=sys.stderr)
|
|
308
|
+
_print_contract()
|
|
309
|
+
sys.exit(0)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def cmd_observation(tool_name: str = "", session_id: str = "") -> None:
|
|
313
|
+
"""Auto-collect observation from tool usage."""
|
|
314
|
+
data = _read_stdin_json()
|
|
315
|
+
payload = _hook_payload(data)
|
|
316
|
+
tool_name = _tool_name(data, tool_name)
|
|
317
|
+
|
|
318
|
+
# Rate limit
|
|
319
|
+
rate_file = _rate_file()
|
|
320
|
+
if rate_file.exists():
|
|
321
|
+
try:
|
|
322
|
+
last = float(rate_file.read_text().strip())
|
|
323
|
+
if time.time() - last < _RATE_LIMIT_SECONDS:
|
|
324
|
+
_print_contract()
|
|
325
|
+
sys.exit(0)
|
|
326
|
+
except (ValueError, OSError):
|
|
327
|
+
pass
|
|
328
|
+
|
|
329
|
+
if tool_name == "Bash" and "command" in payload:
|
|
330
|
+
tool_input = f"Bash: {str(payload['command'])[:200]}"
|
|
331
|
+
elif tool_name in ("Read", "Edit", "Write") and "file_path" in payload:
|
|
332
|
+
tool_input = f"{tool_name}: {payload['file_path']}"
|
|
333
|
+
else:
|
|
334
|
+
tool_input = json.dumps(payload or data, ensure_ascii=False)[:300]
|
|
335
|
+
|
|
336
|
+
tool_input = _strip_private_tags(tool_input)
|
|
337
|
+
if not tool_input:
|
|
338
|
+
_print_contract()
|
|
339
|
+
sys.exit(0)
|
|
340
|
+
|
|
341
|
+
obs_text = f"[{tool_name}] {tool_input}"
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
runtime = _init_runtime(session_id=session_id)
|
|
345
|
+
runtime.after_response(
|
|
346
|
+
user_message=obs_text,
|
|
347
|
+
assistant_message="Observed Claude Code tool use.",
|
|
348
|
+
metadata={"tool_name": tool_name, "tool_input": payload},
|
|
349
|
+
)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
print(f"[Memplex] observation write skipped: {e}", file=sys.stderr)
|
|
352
|
+
|
|
353
|
+
# Update rate limit
|
|
354
|
+
try:
|
|
355
|
+
rate_file.write_text(str(time.time()))
|
|
356
|
+
except OSError:
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
_print_contract()
|
|
360
|
+
sys.exit(0)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def cmd_summarize() -> None:
|
|
364
|
+
"""Session summary and compaction."""
|
|
365
|
+
service = None
|
|
366
|
+
try:
|
|
367
|
+
service = _init_service()
|
|
368
|
+
compaction = service.compact(scope=os.environ.get("MEMPLEX_COMPACTION_SCOPE", "project"))
|
|
369
|
+
stats = service.stats()
|
|
370
|
+
|
|
371
|
+
summary = (
|
|
372
|
+
"[Memplex] Session complete. "
|
|
373
|
+
f"Memories: {stats.get('total_functions', 0)}, "
|
|
374
|
+
f"Edges: {stats.get('total_edges', 0)}, "
|
|
375
|
+
"Compaction: "
|
|
376
|
+
f"processed={compaction.total_processed}, "
|
|
377
|
+
f"merged={compaction.total_merged}, "
|
|
378
|
+
f"removed={compaction.total_removed}"
|
|
379
|
+
)
|
|
380
|
+
print(summary)
|
|
381
|
+
print(OUTPUT_CONTRACT)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
print(f"[Memplex] summarize: {e}", file=sys.stderr)
|
|
384
|
+
_print_contract()
|
|
385
|
+
finally:
|
|
386
|
+
if service is not None:
|
|
387
|
+
service.stop()
|
|
388
|
+
sys.exit(0)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def main() -> None:
|
|
392
|
+
if len(sys.argv) < 2:
|
|
393
|
+
print("Usage: hook-runner.py <command> [args]", file=sys.stderr)
|
|
394
|
+
sys.exit(1)
|
|
395
|
+
|
|
396
|
+
command = sys.argv[1]
|
|
397
|
+
|
|
398
|
+
if command == "setup":
|
|
399
|
+
cmd_setup()
|
|
400
|
+
elif command == "session-start":
|
|
401
|
+
cmd_session_start()
|
|
402
|
+
elif command == "prompt-submit":
|
|
403
|
+
cmd_prompt_submit()
|
|
404
|
+
elif command == "file-context":
|
|
405
|
+
cmd_file_context()
|
|
406
|
+
elif command == "observation":
|
|
407
|
+
tool_name = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
408
|
+
session_id = sys.argv[3] if len(sys.argv) > 3 else ""
|
|
409
|
+
cmd_observation(tool_name, session_id)
|
|
410
|
+
elif command in ("summarize", "session-stop"):
|
|
411
|
+
cmd_summarize()
|
|
412
|
+
else:
|
|
413
|
+
print(f"Unknown command: {command}", file=sys.stderr)
|
|
414
|
+
sys.exit(1)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
if __name__ == "__main__":
|
|
418
|
+
main()
|