memplex 3.2.0__py3-none-any.whl
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.
- memnex/__init__.py +31 -0
- memnex/__main__.py +6 -0
- memnex/_plugin/.claude-plugin/plugin.json +24 -0
- memnex/_plugin/.mcp.json +9 -0
- memnex/_plugin/__init__.py +0 -0
- memnex/_plugin/hooks/hooks.json +43 -0
- memnex/_plugin/scripts/hook-runner.py +166 -0
- memnex/_plugin/skills/mem-explore/SKILL.md +83 -0
- memnex/_plugin/skills/mem-manage/SKILL.md +92 -0
- memnex/_plugin/skills/mem-search/SKILL.md +85 -0
- memnex/_plugin/skills/mem-write/SKILL.md +78 -0
- memnex/adapters/__init__.py +14 -0
- memnex/adapters/claude_skill.py +169 -0
- memnex/adapters/cli.py +525 -0
- memnex/adapters/http_api.py +314 -0
- memnex/adapters/mcp_server.py +448 -0
- memnex/compaction.py +563 -0
- memnex/config.py +366 -0
- memnex/core/__init__.py +13 -0
- memnex/core/associator/__init__.py +8 -0
- memnex/core/associator/domain_classifier.py +75 -0
- memnex/core/associator/entity_aligner.py +127 -0
- memnex/core/associator/ref_linker.py +197 -0
- memnex/core/associator/term_mapper.py +77 -0
- memnex/core/dictionaries/__init__.py +50 -0
- memnex/core/engine.py +667 -0
- memnex/core/extractors/__init__.py +15 -0
- memnex/core/extractors/docx.py +97 -0
- memnex/core/extractors/image.py +233 -0
- memnex/core/extractors/markdown.py +139 -0
- memnex/core/extractors/pdf.py +133 -0
- memnex/core/extractors/vision_mapper.py +131 -0
- memnex/core/handlers/__init__.py +7 -0
- memnex/core/handlers/clipboard.py +40 -0
- memnex/core/handlers/file_handler.py +62 -0
- memnex/core/handlers/url_handler.py +132 -0
- memnex/llm/__init__.py +25 -0
- memnex/llm/enhancer.py +226 -0
- memnex/llm/fallback_chain.py +87 -0
- memnex/llm/injection_guard.py +178 -0
- memnex/llm/provider.py +130 -0
- memnex/llm/providers/__init__.py +22 -0
- memnex/llm/providers/anthropic.py +135 -0
- memnex/llm/providers/local.py +135 -0
- memnex/llm/providers/rule_based.py +68 -0
- memnex/llm/sanitizer.py +67 -0
- memnex/models/__init__.py +68 -0
- memnex/models/feedback.py +42 -0
- memnex/models/graph.py +33 -0
- memnex/models/memory.py +102 -0
- memnex/models/misc.py +185 -0
- memnex/models/paragraph.py +45 -0
- memnex/models/search.py +51 -0
- memnex/models/source.py +23 -0
- memnex/models/task.py +62 -0
- memnex/processing/__init__.py +1 -0
- memnex/processing/graph_builder.py +278 -0
- memnex/processing/merger/__init__.py +6 -0
- memnex/processing/merger/confidence_calculator.py +127 -0
- memnex/processing/merger/conflict_resolver.py +116 -0
- memnex/retrieval/__init__.py +1 -0
- memnex/retrieval/dedup.py +386 -0
- memnex/retrieval/embedding.py +289 -0
- memnex/retrieval/reranker.py +299 -0
- memnex/service.py +902 -0
- memnex/storage/__init__.py +65 -0
- memnex/storage/base.py +132 -0
- memnex/storage/changelog.py +106 -0
- memnex/storage/feedback.py +486 -0
- memnex/storage/lite/__init__.py +5 -0
- memnex/storage/lite/store.py +606 -0
- memnex/storage/vector.py +265 -0
- memnex/wiki/__init__.py +11 -0
- memnex/wiki/community.py +221 -0
- memnex/wiki/compiler.py +545 -0
- memnex/wiki/generator.py +270 -0
- memnex/wiki/search.py +282 -0
- memnex/worker.py +412 -0
- memplex-3.2.0.dist-info/METADATA +37 -0
- memplex-3.2.0.dist-info/RECORD +83 -0
- memplex-3.2.0.dist-info/WHEEL +5 -0
- memplex-3.2.0.dist-info/entry_points.txt +2 -0
- memplex-3.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""MemNex Claude Skill Adapter -- generates SKILL.md and hook scripts.
|
|
2
|
+
|
|
3
|
+
Generates files that integrate MemNex into Claude Code's skill system:
|
|
4
|
+
|
|
5
|
+
- ``SKILL.md``: skill description with YAML frontmatter and trigger conditions
|
|
6
|
+
- ``hook.sh``: PostToolUse hook script that auto-collects observations
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from memnex.adapters.claude_skill import generate_skill_md, generate_hook_sh
|
|
11
|
+
|
|
12
|
+
skill_content = generate_skill_md()
|
|
13
|
+
hook_content = generate_hook_sh()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import textwrap
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_SKILL_MD_TEMPLATE = textwrap.dedent("""\
|
|
23
|
+
---
|
|
24
|
+
name: memnex
|
|
25
|
+
description: Search and manage MemNex persistent memory. Use when user asks to "search memory", "recall", "remember this", "save", "lookup", or needs knowledge from previous sessions.
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# MemNex Memory Skill
|
|
29
|
+
|
|
30
|
+
Persistent knowledge graph for multi-agent workflows. Store, query, and
|
|
31
|
+
manage knowledge that persists across sessions.
|
|
32
|
+
|
|
33
|
+
## When to Use
|
|
34
|
+
|
|
35
|
+
Activate when the user:
|
|
36
|
+
- Asks to find or recall information from past sessions
|
|
37
|
+
- Provides content and asks to "remember" or "save" it
|
|
38
|
+
- Wants to review, correct, or update existing memories
|
|
39
|
+
- Uses keywords: "memnex", "memory", "remember", "recall", "lookup"
|
|
40
|
+
|
|
41
|
+
## 3-Layer Retrieval (ALWAYS Follow)
|
|
42
|
+
|
|
43
|
+
**NEVER fetch full details without filtering first.**
|
|
44
|
+
|
|
45
|
+
### Step 1: Search -- Get Index with IDs
|
|
46
|
+
|
|
47
|
+
Use the `memory_search` MCP tool:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
memory_search(query="search text", top_k=10)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Returns: IDs, names, relevance scores (~50-100 tokens/result)
|
|
54
|
+
|
|
55
|
+
### Step 2: Filter -- Review Results
|
|
56
|
+
|
|
57
|
+
Pick relevant IDs from search results. Discard the rest.
|
|
58
|
+
|
|
59
|
+
### Step 3: Fetch -- Get Full Details for Filtered IDs
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
memory_get(memory_id="func_abc123")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Returns: Complete memory with all fields (~500-1000 tokens)
|
|
66
|
+
|
|
67
|
+
## Write Memory
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
memory_add(content="text to remember", source_type="text")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Feedback and Maintenance
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
memory_feedback(memory_id="...", role="trigger", index=0, verdict="correct")
|
|
77
|
+
memory_pending_reviews(limit=20)
|
|
78
|
+
memory_health()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## CLI Commands
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
memnex query "search text" # Search memories
|
|
85
|
+
memnex write --text "content" # Write memory
|
|
86
|
+
memnex get <memory_id> # Get details
|
|
87
|
+
memnex health # Health check
|
|
88
|
+
memnex stats # Statistics
|
|
89
|
+
memnex compact --scope project # Run compaction
|
|
90
|
+
```
|
|
91
|
+
""")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def generate_skill_md(output_path: Optional[str] = None) -> str:
|
|
95
|
+
content = _SKILL_MD_TEMPLATE.strip() + "\n"
|
|
96
|
+
|
|
97
|
+
if output_path is not None:
|
|
98
|
+
import os
|
|
99
|
+
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
|
100
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
101
|
+
f.write(content)
|
|
102
|
+
|
|
103
|
+
return content
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_HOOK_SH_TEMPLATE = textwrap.dedent("""\
|
|
107
|
+
#!/usr/bin/env bash
|
|
108
|
+
# MemNex PostToolUse Hook -- auto-collect observations
|
|
109
|
+
#
|
|
110
|
+
# Register in plugin/hooks/hooks.json.
|
|
111
|
+
#
|
|
112
|
+
# Environment variables provided by Claude Code:
|
|
113
|
+
# MEMNEX_TOOL_NAME - name of the tool that was called
|
|
114
|
+
# MEMNEX_SESSION_ID - current session identifier
|
|
115
|
+
|
|
116
|
+
set -euo pipefail
|
|
117
|
+
|
|
118
|
+
# Rate limit: skip if last observation was less than 30 seconds ago
|
|
119
|
+
RATE_FILE="/tmp/.memnex_last_obs_${MEMNEX_SESSION_ID:-default}"
|
|
120
|
+
if [ -f "$RATE_FILE" ]; then
|
|
121
|
+
LAST=$(cat "$RATE_FILE" 2>/dev/null || echo 0)
|
|
122
|
+
NOW=$(date +%s)
|
|
123
|
+
DIFF=$((NOW - LAST))
|
|
124
|
+
if [ "$DIFF" -lt 30 ]; then
|
|
125
|
+
exit 0
|
|
126
|
+
fi
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
TOOL_NAME="${MEMNEX_TOOL_NAME:-unknown}"
|
|
130
|
+
TOOL_INPUT="${MEMNEX_TOOL_INPUT:-}"
|
|
131
|
+
|
|
132
|
+
# Skip low-value tools
|
|
133
|
+
case "$TOOL_NAME" in
|
|
134
|
+
Read|read) exit 0 ;;
|
|
135
|
+
esac
|
|
136
|
+
|
|
137
|
+
# Build observation text from tool input
|
|
138
|
+
OBS_TEXT="[$TOOL_NAME] $TOOL_INPUT"
|
|
139
|
+
|
|
140
|
+
# Truncate to reasonable length
|
|
141
|
+
if [ ${#OBS_TEXT} -gt 500 ]; then
|
|
142
|
+
OBS_TEXT="${OBS_TEXT:0:500}..."
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# Store observation via CLI
|
|
146
|
+
if command -v memnex &>/dev/null; then
|
|
147
|
+
memnex write --text "$OBS_TEXT" --output json 2>/dev/null || true
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# Update rate limit timestamp
|
|
151
|
+
date +%s > "$RATE_FILE"
|
|
152
|
+
""")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def generate_hook_sh(output_path: Optional[str] = None) -> str:
|
|
156
|
+
content = _HOOK_SH_TEMPLATE.strip() + "\n"
|
|
157
|
+
|
|
158
|
+
if output_path is not None:
|
|
159
|
+
import os
|
|
160
|
+
import stat
|
|
161
|
+
|
|
162
|
+
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
|
163
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
164
|
+
f.write(content)
|
|
165
|
+
|
|
166
|
+
st = os.stat(output_path)
|
|
167
|
+
os.chmod(output_path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
168
|
+
|
|
169
|
+
return content
|
memnex/adapters/cli.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""MemNex CLI -- command-line interface using argparse.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
memnex query "login function"
|
|
6
|
+
memnex write --text "some observation"
|
|
7
|
+
memnex write --file ./notes.txt
|
|
8
|
+
memnex write --url https://example.com/doc
|
|
9
|
+
memnex get func_abc123
|
|
10
|
+
memnex delete func_abc123
|
|
11
|
+
memnex feedback func_abc123 --role trigger --index 0 --verdict correct
|
|
12
|
+
memnex pending
|
|
13
|
+
memnex compact --scope project
|
|
14
|
+
memnex health
|
|
15
|
+
memnex stats
|
|
16
|
+
memnex setup # Install as Claude Code plugin
|
|
17
|
+
memnex unsetup # Uninstall Claude Code plugin
|
|
18
|
+
|
|
19
|
+
Global options::
|
|
20
|
+
|
|
21
|
+
--config <path> Path to config YAML file
|
|
22
|
+
--output json|table Output format (default: table)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import sys
|
|
32
|
+
from dataclasses import asdict
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Optional, Sequence
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Helpers ─────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _make_service(config_path: Optional[str] = None):
|
|
41
|
+
"""Create and return a MemNexService instance."""
|
|
42
|
+
from memnex.config import load_config
|
|
43
|
+
from memnex.service import MemNexService
|
|
44
|
+
|
|
45
|
+
config = load_config(path=config_path)
|
|
46
|
+
return MemNexService(config=config)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fmt(data, output: str) -> str:
|
|
50
|
+
"""Format *data* for the chosen output mode."""
|
|
51
|
+
if output == "json":
|
|
52
|
+
return json.dumps(data, indent=2, default=str, ensure_ascii=False)
|
|
53
|
+
|
|
54
|
+
# table / plain text
|
|
55
|
+
if isinstance(data, list):
|
|
56
|
+
if not data:
|
|
57
|
+
return "(empty)"
|
|
58
|
+
lines = []
|
|
59
|
+
for item in data:
|
|
60
|
+
if isinstance(item, dict):
|
|
61
|
+
lines.append(_dict_to_table(item))
|
|
62
|
+
else:
|
|
63
|
+
lines.append(str(item))
|
|
64
|
+
return "\n---\n".join(lines)
|
|
65
|
+
|
|
66
|
+
if isinstance(data, dict):
|
|
67
|
+
return _dict_to_table(data)
|
|
68
|
+
|
|
69
|
+
return str(data)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _dict_to_table(d: dict, indent: int = 0) -> str:
|
|
73
|
+
"""Recursively format a dict as indented key-value lines."""
|
|
74
|
+
prefix = " " * indent
|
|
75
|
+
lines = []
|
|
76
|
+
for k, v in d.items():
|
|
77
|
+
if isinstance(v, dict):
|
|
78
|
+
lines.append(f"{prefix}{k}:")
|
|
79
|
+
lines.append(_dict_to_table(v, indent + 1))
|
|
80
|
+
elif isinstance(v, list):
|
|
81
|
+
lines.append(f"{prefix}{k}:")
|
|
82
|
+
for item in v:
|
|
83
|
+
if isinstance(item, dict):
|
|
84
|
+
lines.append(_dict_to_table(item, indent + 1))
|
|
85
|
+
else:
|
|
86
|
+
lines.append(f"{prefix} - {item}")
|
|
87
|
+
else:
|
|
88
|
+
lines.append(f"{prefix}{k}: {v}")
|
|
89
|
+
return "\n".join(lines)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _result_to_dict(result) -> dict:
|
|
93
|
+
"""Convert a SearchResult / QueryResult / dataclass to a dict."""
|
|
94
|
+
if hasattr(result, "__dataclass_fields__"):
|
|
95
|
+
return asdict(result)
|
|
96
|
+
if isinstance(result, dict):
|
|
97
|
+
return result
|
|
98
|
+
return {"value": str(result)}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _dataclass_to_dict(obj):
|
|
102
|
+
"""Recursively convert dataclasses to dicts."""
|
|
103
|
+
if hasattr(obj, "__dataclass_fields__"):
|
|
104
|
+
return asdict(obj)
|
|
105
|
+
if isinstance(obj, list):
|
|
106
|
+
return [_dataclass_to_dict(item) for item in obj]
|
|
107
|
+
if isinstance(obj, dict):
|
|
108
|
+
return {k: _dataclass_to_dict(v) for k, v in obj.items()}
|
|
109
|
+
return obj
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── Command implementations ────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def cmd_query(args: argparse.Namespace) -> int:
|
|
116
|
+
"""Execute a memory query."""
|
|
117
|
+
svc = _make_service(getattr(args, "config", None))
|
|
118
|
+
try:
|
|
119
|
+
result = svc.query(
|
|
120
|
+
text=args.text,
|
|
121
|
+
top_k=getattr(args, "top_k", 10),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
out = []
|
|
125
|
+
for r in result.results:
|
|
126
|
+
out.append({
|
|
127
|
+
"id": r.func_id,
|
|
128
|
+
"name": r.name,
|
|
129
|
+
"relevance": round(r.relevance_score, 4),
|
|
130
|
+
"summary": r.summary,
|
|
131
|
+
"scope": r.domain,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
print(_fmt({
|
|
135
|
+
"total": len(out),
|
|
136
|
+
"scope": result.scope.value if hasattr(result.scope, "value") else str(result.scope),
|
|
137
|
+
"latency_ms": result.latency_ms,
|
|
138
|
+
"results": out,
|
|
139
|
+
}, args.output))
|
|
140
|
+
return 0
|
|
141
|
+
finally:
|
|
142
|
+
svc.stop()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def cmd_write(args: argparse.Namespace) -> int:
|
|
146
|
+
"""Write new content into memory."""
|
|
147
|
+
svc = _make_service(getattr(args, "config", None))
|
|
148
|
+
try:
|
|
149
|
+
if args.text:
|
|
150
|
+
content = args.text
|
|
151
|
+
source_type = "text"
|
|
152
|
+
elif args.file:
|
|
153
|
+
with open(args.file, "r", encoding="utf-8") as f:
|
|
154
|
+
content = f.read()
|
|
155
|
+
source_type = "file"
|
|
156
|
+
elif args.url:
|
|
157
|
+
content = args.url
|
|
158
|
+
source_type = "url"
|
|
159
|
+
else:
|
|
160
|
+
print("Error: provide --text, --file, or --url", file=sys.stderr)
|
|
161
|
+
return 1
|
|
162
|
+
|
|
163
|
+
result = svc.write_text(text=content, source_type=source_type)
|
|
164
|
+
|
|
165
|
+
out = {
|
|
166
|
+
"functions_extracted": len(result.functions),
|
|
167
|
+
"edges": len(result.graph.edges),
|
|
168
|
+
"function_ids": [f.id for f in result.functions],
|
|
169
|
+
}
|
|
170
|
+
print(_fmt(out, args.output))
|
|
171
|
+
return 0
|
|
172
|
+
finally:
|
|
173
|
+
svc.stop()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def cmd_get(args: argparse.Namespace) -> int:
|
|
177
|
+
"""Retrieve a single memory by ID."""
|
|
178
|
+
svc = _make_service(getattr(args, "config", None))
|
|
179
|
+
try:
|
|
180
|
+
func = svc.get(args.memory_id)
|
|
181
|
+
if func is None:
|
|
182
|
+
print(f"Memory not found: {args.memory_id}", file=sys.stderr)
|
|
183
|
+
return 1
|
|
184
|
+
|
|
185
|
+
print(_fmt(_dataclass_to_dict(func), args.output))
|
|
186
|
+
return 0
|
|
187
|
+
finally:
|
|
188
|
+
svc.stop()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def cmd_delete(args: argparse.Namespace) -> int:
|
|
192
|
+
"""Delete a memory by ID."""
|
|
193
|
+
svc = _make_service(getattr(args, "config", None))
|
|
194
|
+
try:
|
|
195
|
+
svc.delete(args.memory_id)
|
|
196
|
+
print(_fmt({"status": "deleted", "id": args.memory_id}, args.output))
|
|
197
|
+
return 0
|
|
198
|
+
finally:
|
|
199
|
+
svc.stop()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def cmd_feedback(args: argparse.Namespace) -> int:
|
|
203
|
+
"""Submit feedback for a memory field value."""
|
|
204
|
+
svc = _make_service(getattr(args, "config", None))
|
|
205
|
+
try:
|
|
206
|
+
svc.submit_feedback(
|
|
207
|
+
memory_id=args.memory_id,
|
|
208
|
+
field_role=args.role,
|
|
209
|
+
value_index=args.index,
|
|
210
|
+
verdict=args.verdict,
|
|
211
|
+
)
|
|
212
|
+
print(_fmt({
|
|
213
|
+
"status": "recorded",
|
|
214
|
+
"memory_id": args.memory_id,
|
|
215
|
+
"role": args.role,
|
|
216
|
+
"index": args.index,
|
|
217
|
+
"verdict": args.verdict,
|
|
218
|
+
}, args.output))
|
|
219
|
+
return 0
|
|
220
|
+
finally:
|
|
221
|
+
svc.stop()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def cmd_pending(args: argparse.Namespace) -> int:
|
|
225
|
+
"""List pending reviews."""
|
|
226
|
+
svc = _make_service(getattr(args, "config", None))
|
|
227
|
+
try:
|
|
228
|
+
reviews = svc.get_pending_reviews()
|
|
229
|
+
out = [_dataclass_to_dict(r) for r in reviews]
|
|
230
|
+
print(_fmt({"total": len(out), "reviews": out}, args.output))
|
|
231
|
+
return 0
|
|
232
|
+
finally:
|
|
233
|
+
svc.stop()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def cmd_compact(args: argparse.Namespace) -> int:
|
|
237
|
+
"""Run the compaction pipeline."""
|
|
238
|
+
svc = _make_service(getattr(args, "config", None))
|
|
239
|
+
try:
|
|
240
|
+
result = svc.compact(scope=getattr(args, "scope", "project"))
|
|
241
|
+
out = _dataclass_to_dict(result)
|
|
242
|
+
print(_fmt(out, args.output))
|
|
243
|
+
return 0
|
|
244
|
+
finally:
|
|
245
|
+
svc.stop()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def cmd_health(args: argparse.Namespace) -> int:
|
|
249
|
+
"""Health check."""
|
|
250
|
+
svc = _make_service(getattr(args, "config", None))
|
|
251
|
+
try:
|
|
252
|
+
info = svc.health()
|
|
253
|
+
print(_fmt(info, args.output))
|
|
254
|
+
return 0 if info.get("status") == "ok" else 1
|
|
255
|
+
finally:
|
|
256
|
+
svc.stop()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def cmd_stats(args: argparse.Namespace) -> int:
|
|
260
|
+
"""Display statistics."""
|
|
261
|
+
svc = _make_service(getattr(args, "config", None))
|
|
262
|
+
try:
|
|
263
|
+
info = svc.stats()
|
|
264
|
+
print(_fmt(info, args.output))
|
|
265
|
+
return 0
|
|
266
|
+
finally:
|
|
267
|
+
svc.stop()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ── Claude Code Plugin Setup ────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
_PLUGIN_AUTHOR = "articultur"
|
|
273
|
+
_PLUGIN_NAME = "memnex"
|
|
274
|
+
_MARKETPLACE_JSON = """{
|
|
275
|
+
"name": "memplex",
|
|
276
|
+
"interface": {
|
|
277
|
+
"displayName": "Memplex (local)"
|
|
278
|
+
},
|
|
279
|
+
"plugins": [
|
|
280
|
+
{
|
|
281
|
+
"name": "memplex",
|
|
282
|
+
"source": {
|
|
283
|
+
"source": "local",
|
|
284
|
+
"path": "./plugin"
|
|
285
|
+
},
|
|
286
|
+
"policy": {
|
|
287
|
+
"installation": "AVAILABLE",
|
|
288
|
+
"authentication": "ON_INSTALL"
|
|
289
|
+
},
|
|
290
|
+
"category": "Productivity"
|
|
291
|
+
}
|
|
292
|
+
]
|
|
293
|
+
}
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _get_plugin_source_dir() -> Path:
|
|
298
|
+
"""Find the plugin directory within the memnex package."""
|
|
299
|
+
# Installed via pip: use bundled memnex/_plugin/
|
|
300
|
+
package_dir = Path(__file__).resolve().parent.parent
|
|
301
|
+
bundled = package_dir / "_plugin"
|
|
302
|
+
if bundled.exists() and (bundled / "hooks").exists():
|
|
303
|
+
return bundled
|
|
304
|
+
# Development mode: use project root plugin/
|
|
305
|
+
dev_plugin = package_dir / "plugin"
|
|
306
|
+
if dev_plugin.exists() and (dev_plugin / "hooks").exists():
|
|
307
|
+
return dev_plugin
|
|
308
|
+
raise FileNotFoundError("Cannot find plugin directory in memnex package")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _get_marketplace_dir() -> Path:
|
|
312
|
+
"""Return the Claude Code marketplace target directory."""
|
|
313
|
+
claude_dir = Path(os.environ.get("CLAUDE_CONFIG_DIR", Path.home() / ".claude"))
|
|
314
|
+
return claude_dir / "plugins" / "marketplaces" / _PLUGIN_AUTHOR
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def cmd_setup(args: argparse.Namespace) -> int:
|
|
318
|
+
"""Install MemNex as a Claude Code plugin."""
|
|
319
|
+
market_dir = _get_marketplace_dir()
|
|
320
|
+
plugin_target = market_dir / "plugin"
|
|
321
|
+
|
|
322
|
+
print("Memplex Plugin Setup")
|
|
323
|
+
print("=" * 40)
|
|
324
|
+
|
|
325
|
+
# 1. Check Python dependencies
|
|
326
|
+
print("\n[1/4] Checking dependencies...")
|
|
327
|
+
try:
|
|
328
|
+
import yaml # noqa: F401
|
|
329
|
+
import numpy # noqa: F401
|
|
330
|
+
print(" Core dependencies: OK")
|
|
331
|
+
except ImportError as e:
|
|
332
|
+
print(f" Missing dependency: {e}")
|
|
333
|
+
print(f" Run: pip install memnex[embedding]")
|
|
334
|
+
return 1
|
|
335
|
+
|
|
336
|
+
# 2. Find and copy plugin directory
|
|
337
|
+
print("\n[2/4] Installing plugin files...")
|
|
338
|
+
try:
|
|
339
|
+
source = _get_plugin_source_dir()
|
|
340
|
+
except FileNotFoundError as e:
|
|
341
|
+
print(f" Error: {e}")
|
|
342
|
+
return 1
|
|
343
|
+
|
|
344
|
+
if plugin_target.exists():
|
|
345
|
+
shutil.rmtree(plugin_target)
|
|
346
|
+
|
|
347
|
+
def _ignore_patterns(_dir, files):
|
|
348
|
+
return [f for f in files if f == "__pycache__" or f.endswith(".pyc")]
|
|
349
|
+
|
|
350
|
+
shutil.copytree(source, plugin_target, symlinks=False, ignore=_ignore_patterns)
|
|
351
|
+
print(f" Installed to: {plugin_target}")
|
|
352
|
+
|
|
353
|
+
# 3. Write marketplace.json
|
|
354
|
+
print("\n[3/4] Registering with Claude Code...")
|
|
355
|
+
market_json = market_dir / "marketplace.json"
|
|
356
|
+
market_dir.mkdir(parents=True, exist_ok=True)
|
|
357
|
+
market_json.write_text(_MARKETPLACE_JSON.strip() + "\n")
|
|
358
|
+
print(f" Marketplace: {market_json}")
|
|
359
|
+
|
|
360
|
+
# 4. Write install marker
|
|
361
|
+
print("\n[4/4] Writing install marker...")
|
|
362
|
+
marker = market_dir / ".install-version"
|
|
363
|
+
from importlib.metadata import version as pkg_version
|
|
364
|
+
try:
|
|
365
|
+
ver = pkg_version("memnex")
|
|
366
|
+
except Exception:
|
|
367
|
+
ver = "3.2.0"
|
|
368
|
+
marker.write_text(json.dumps({
|
|
369
|
+
"version": ver,
|
|
370
|
+
"installedAt": __import__("datetime").datetime.now().isoformat(),
|
|
371
|
+
}, indent=2))
|
|
372
|
+
print(f" Version: {ver}")
|
|
373
|
+
|
|
374
|
+
print("\n" + "=" * 40)
|
|
375
|
+
print("Memplex plugin installed successfully!")
|
|
376
|
+
print("\nWhat was configured:")
|
|
377
|
+
print(f" - Hooks: {plugin_target}/hooks/hooks.json")
|
|
378
|
+
print(f" - MCP: {plugin_target}/.mcp.json")
|
|
379
|
+
print(f" - Skills: {plugin_target}/skills/*/SKILL.md")
|
|
380
|
+
print(f" - Manifest: {plugin_target}/../.claude-plugin/plugin.json")
|
|
381
|
+
print("\nRestart Claude Code to activate the plugin.")
|
|
382
|
+
return 0
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def cmd_unsetup(args: argparse.Namespace) -> int:
|
|
386
|
+
"""Uninstall MemNex Claude Code plugin."""
|
|
387
|
+
market_dir = _get_marketplace_dir()
|
|
388
|
+
|
|
389
|
+
print("Memplex Plugin Uninstall")
|
|
390
|
+
print("=" * 40)
|
|
391
|
+
|
|
392
|
+
if not market_dir.exists():
|
|
393
|
+
print(" Plugin not installed (directory not found).")
|
|
394
|
+
return 0
|
|
395
|
+
|
|
396
|
+
shutil.rmtree(market_dir)
|
|
397
|
+
print(f" Removed: {market_dir}")
|
|
398
|
+
print("\nMemplex plugin uninstalled. Restart Claude Code to apply.")
|
|
399
|
+
return 0
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# ── Argument parser ────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
406
|
+
"""Build and return the top-level argument parser."""
|
|
407
|
+
parser = argparse.ArgumentParser(
|
|
408
|
+
prog="memnex",
|
|
409
|
+
description="MemNex -- multi-agent memory system",
|
|
410
|
+
)
|
|
411
|
+
parser.add_argument("--config", default=None, help="Path to config YAML file")
|
|
412
|
+
parser.add_argument(
|
|
413
|
+
"--output",
|
|
414
|
+
choices=["json", "table"],
|
|
415
|
+
default="table",
|
|
416
|
+
help="Output format (default: table)",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
sub = parser.add_subparsers(dest="command", help="Available commands")
|
|
420
|
+
|
|
421
|
+
# -- query --
|
|
422
|
+
p_query = sub.add_parser("query", help="Query memory")
|
|
423
|
+
p_query.add_argument("text", help="Query text")
|
|
424
|
+
p_query.add_argument("--top-k", type=int, default=10, help="Max results")
|
|
425
|
+
|
|
426
|
+
# -- write --
|
|
427
|
+
p_write = sub.add_parser("write", help="Write content to memory")
|
|
428
|
+
p_write.add_argument("--text", help="Raw text to write")
|
|
429
|
+
p_write.add_argument("--file", help="File path to read and write")
|
|
430
|
+
p_write.add_argument("--url", help="URL to write")
|
|
431
|
+
|
|
432
|
+
# -- get --
|
|
433
|
+
p_get = sub.add_parser("get", help="Get memory by ID")
|
|
434
|
+
p_get.add_argument("memory_id", help="Memory ID")
|
|
435
|
+
|
|
436
|
+
# -- delete --
|
|
437
|
+
p_del = sub.add_parser("delete", help="Delete memory by ID")
|
|
438
|
+
p_del.add_argument("memory_id", help="Memory ID")
|
|
439
|
+
|
|
440
|
+
# -- feedback --
|
|
441
|
+
p_fb = sub.add_parser("feedback", help="Submit feedback on a memory field")
|
|
442
|
+
p_fb.add_argument("memory_id", help="Memory ID")
|
|
443
|
+
p_fb.add_argument("--role", required=True, help="Field role (trigger|action|condition|benefit)")
|
|
444
|
+
p_fb.add_argument("--index", type=int, required=True, help="Value index")
|
|
445
|
+
p_fb.add_argument(
|
|
446
|
+
"--verdict",
|
|
447
|
+
required=True,
|
|
448
|
+
choices=["correct", "wrong"],
|
|
449
|
+
help="Verdict",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# -- pending --
|
|
453
|
+
sub.add_parser("pending", help="List pending reviews")
|
|
454
|
+
|
|
455
|
+
# -- compact --
|
|
456
|
+
p_compact = sub.add_parser("compact", help="Run compaction pipeline")
|
|
457
|
+
p_compact.add_argument(
|
|
458
|
+
"--scope",
|
|
459
|
+
default="project",
|
|
460
|
+
choices=["session", "project", "global"],
|
|
461
|
+
help="Compaction scope (default: project)",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# -- health --
|
|
465
|
+
sub.add_parser("health", help="Health check")
|
|
466
|
+
|
|
467
|
+
# -- stats --
|
|
468
|
+
sub.add_parser("stats", help="Show statistics")
|
|
469
|
+
|
|
470
|
+
# -- setup --
|
|
471
|
+
p_setup = sub.add_parser("setup", help="Install MemNex as a Claude Code plugin")
|
|
472
|
+
p_setup.add_argument("--uninstall", action="store_true", help="Uninstall the plugin")
|
|
473
|
+
|
|
474
|
+
# -- unsetup --
|
|
475
|
+
sub.add_parser("unsetup", help="Uninstall MemNex Claude Code plugin")
|
|
476
|
+
|
|
477
|
+
return parser
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
# ── Entry point ─────────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
484
|
+
"""CLI entry point.
|
|
485
|
+
|
|
486
|
+
Parameters
|
|
487
|
+
----------
|
|
488
|
+
argv:
|
|
489
|
+
Argument list. Defaults to ``sys.argv[1:]``.
|
|
490
|
+
"""
|
|
491
|
+
parser = build_parser()
|
|
492
|
+
args = parser.parse_args(argv)
|
|
493
|
+
|
|
494
|
+
if args.command is None:
|
|
495
|
+
parser.print_help()
|
|
496
|
+
return 0
|
|
497
|
+
|
|
498
|
+
dispatch = {
|
|
499
|
+
"query": cmd_query,
|
|
500
|
+
"write": cmd_write,
|
|
501
|
+
"get": cmd_get,
|
|
502
|
+
"delete": cmd_delete,
|
|
503
|
+
"feedback": cmd_feedback,
|
|
504
|
+
"pending": cmd_pending,
|
|
505
|
+
"compact": cmd_compact,
|
|
506
|
+
"health": cmd_health,
|
|
507
|
+
"stats": cmd_stats,
|
|
508
|
+
"setup": cmd_setup,
|
|
509
|
+
"unsetup": cmd_unsetup,
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
handler = dispatch.get(args.command)
|
|
513
|
+
if handler is None:
|
|
514
|
+
parser.print_help()
|
|
515
|
+
return 1
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
return handler(args)
|
|
519
|
+
except Exception as exc:
|
|
520
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
521
|
+
return 1
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
if __name__ == "__main__":
|
|
525
|
+
sys.exit(main())
|