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.
Files changed (83) hide show
  1. memnex/__init__.py +31 -0
  2. memnex/__main__.py +6 -0
  3. memnex/_plugin/.claude-plugin/plugin.json +24 -0
  4. memnex/_plugin/.mcp.json +9 -0
  5. memnex/_plugin/__init__.py +0 -0
  6. memnex/_plugin/hooks/hooks.json +43 -0
  7. memnex/_plugin/scripts/hook-runner.py +166 -0
  8. memnex/_plugin/skills/mem-explore/SKILL.md +83 -0
  9. memnex/_plugin/skills/mem-manage/SKILL.md +92 -0
  10. memnex/_plugin/skills/mem-search/SKILL.md +85 -0
  11. memnex/_plugin/skills/mem-write/SKILL.md +78 -0
  12. memnex/adapters/__init__.py +14 -0
  13. memnex/adapters/claude_skill.py +169 -0
  14. memnex/adapters/cli.py +525 -0
  15. memnex/adapters/http_api.py +314 -0
  16. memnex/adapters/mcp_server.py +448 -0
  17. memnex/compaction.py +563 -0
  18. memnex/config.py +366 -0
  19. memnex/core/__init__.py +13 -0
  20. memnex/core/associator/__init__.py +8 -0
  21. memnex/core/associator/domain_classifier.py +75 -0
  22. memnex/core/associator/entity_aligner.py +127 -0
  23. memnex/core/associator/ref_linker.py +197 -0
  24. memnex/core/associator/term_mapper.py +77 -0
  25. memnex/core/dictionaries/__init__.py +50 -0
  26. memnex/core/engine.py +667 -0
  27. memnex/core/extractors/__init__.py +15 -0
  28. memnex/core/extractors/docx.py +97 -0
  29. memnex/core/extractors/image.py +233 -0
  30. memnex/core/extractors/markdown.py +139 -0
  31. memnex/core/extractors/pdf.py +133 -0
  32. memnex/core/extractors/vision_mapper.py +131 -0
  33. memnex/core/handlers/__init__.py +7 -0
  34. memnex/core/handlers/clipboard.py +40 -0
  35. memnex/core/handlers/file_handler.py +62 -0
  36. memnex/core/handlers/url_handler.py +132 -0
  37. memnex/llm/__init__.py +25 -0
  38. memnex/llm/enhancer.py +226 -0
  39. memnex/llm/fallback_chain.py +87 -0
  40. memnex/llm/injection_guard.py +178 -0
  41. memnex/llm/provider.py +130 -0
  42. memnex/llm/providers/__init__.py +22 -0
  43. memnex/llm/providers/anthropic.py +135 -0
  44. memnex/llm/providers/local.py +135 -0
  45. memnex/llm/providers/rule_based.py +68 -0
  46. memnex/llm/sanitizer.py +67 -0
  47. memnex/models/__init__.py +68 -0
  48. memnex/models/feedback.py +42 -0
  49. memnex/models/graph.py +33 -0
  50. memnex/models/memory.py +102 -0
  51. memnex/models/misc.py +185 -0
  52. memnex/models/paragraph.py +45 -0
  53. memnex/models/search.py +51 -0
  54. memnex/models/source.py +23 -0
  55. memnex/models/task.py +62 -0
  56. memnex/processing/__init__.py +1 -0
  57. memnex/processing/graph_builder.py +278 -0
  58. memnex/processing/merger/__init__.py +6 -0
  59. memnex/processing/merger/confidence_calculator.py +127 -0
  60. memnex/processing/merger/conflict_resolver.py +116 -0
  61. memnex/retrieval/__init__.py +1 -0
  62. memnex/retrieval/dedup.py +386 -0
  63. memnex/retrieval/embedding.py +289 -0
  64. memnex/retrieval/reranker.py +299 -0
  65. memnex/service.py +902 -0
  66. memnex/storage/__init__.py +65 -0
  67. memnex/storage/base.py +132 -0
  68. memnex/storage/changelog.py +106 -0
  69. memnex/storage/feedback.py +486 -0
  70. memnex/storage/lite/__init__.py +5 -0
  71. memnex/storage/lite/store.py +606 -0
  72. memnex/storage/vector.py +265 -0
  73. memnex/wiki/__init__.py +11 -0
  74. memnex/wiki/community.py +221 -0
  75. memnex/wiki/compiler.py +545 -0
  76. memnex/wiki/generator.py +270 -0
  77. memnex/wiki/search.py +282 -0
  78. memnex/worker.py +412 -0
  79. memplex-3.2.0.dist-info/METADATA +37 -0
  80. memplex-3.2.0.dist-info/RECORD +83 -0
  81. memplex-3.2.0.dist-info/WHEEL +5 -0
  82. memplex-3.2.0.dist-info/entry_points.txt +2 -0
  83. 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())