memplex 3.2.2__tar.gz → 3.2.4__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.
Files changed (120) hide show
  1. {memplex-3.2.2 → memplex-3.2.4}/PKG-INFO +1 -1
  2. {memplex-3.2.2 → memplex-3.2.4}/README.md +35 -29
  3. {memplex-3.2.2 → memplex-3.2.4}/memplex/_plugin/.claude-plugin/plugin.json +1 -1
  4. {memplex-3.2.2 → memplex-3.2.4}/memplex/adapters/agent_installer.py +38 -20
  5. {memplex-3.2.2 → memplex-3.2.4}/memplex/adapters/cli.py +58 -80
  6. {memplex-3.2.2 → memplex-3.2.4}/memplex/adapters/mcp_server.py +3 -7
  7. {memplex-3.2.2 → memplex-3.2.4}/memplex/compaction.py +18 -13
  8. {memplex-3.2.2 → memplex-3.2.4}/memplex/retrieval/reranker.py +11 -3
  9. {memplex-3.2.2 → memplex-3.2.4}/memplex.egg-info/PKG-INFO +1 -1
  10. {memplex-3.2.2 → memplex-3.2.4}/pyproject.toml +1 -1
  11. {memplex-3.2.2 → memplex-3.2.4}/tests/test_agent_hot_paths.py +62 -0
  12. {memplex-3.2.2 → memplex-3.2.4}/tests/test_agent_runtime.py +24 -0
  13. {memplex-3.2.2 → memplex-3.2.4}/tests/test_hooks.py +48 -51
  14. {memplex-3.2.2 → memplex-3.2.4}/tests/test_install_scripts.py +68 -0
  15. {memplex-3.2.2 → memplex-3.2.4}/LICENSE +0 -0
  16. {memplex-3.2.2 → memplex-3.2.4}/memplex/__init__.py +0 -0
  17. {memplex-3.2.2 → memplex-3.2.4}/memplex/__main__.py +0 -0
  18. {memplex-3.2.2 → memplex-3.2.4}/memplex/_plugin/.mcp.json +0 -0
  19. {memplex-3.2.2 → memplex-3.2.4}/memplex/_plugin/__init__.py +0 -0
  20. {memplex-3.2.2 → memplex-3.2.4}/memplex/_plugin/hooks/hooks.json +0 -0
  21. {memplex-3.2.2 → memplex-3.2.4}/memplex/_plugin/scripts/hook-runner.py +0 -0
  22. {memplex-3.2.2 → memplex-3.2.4}/memplex/_plugin/skills/mem-explore/SKILL.md +0 -0
  23. {memplex-3.2.2 → memplex-3.2.4}/memplex/_plugin/skills/mem-manage/SKILL.md +0 -0
  24. {memplex-3.2.2 → memplex-3.2.4}/memplex/_plugin/skills/mem-search/SKILL.md +0 -0
  25. {memplex-3.2.2 → memplex-3.2.4}/memplex/_plugin/skills/mem-write/SKILL.md +0 -0
  26. {memplex-3.2.2 → memplex-3.2.4}/memplex/adapters/__init__.py +0 -0
  27. {memplex-3.2.2 → memplex-3.2.4}/memplex/adapters/agent_runtime.py +0 -0
  28. {memplex-3.2.2 → memplex-3.2.4}/memplex/adapters/claude_skill.py +0 -0
  29. {memplex-3.2.2 → memplex-3.2.4}/memplex/adapters/http_api.py +0 -0
  30. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/__init__.py +0 -0
  31. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/base.py +0 -0
  32. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/benchmark_cli.py +0 -0
  33. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/evaluator.py +0 -0
  34. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/loader.py +0 -0
  35. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/locomo.py +0 -0
  36. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/memory_eval.py +0 -0
  37. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/memory_metrics.py +0 -0
  38. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/metrics.py +0 -0
  39. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/nq_trivia.py +0 -0
  40. {memplex-3.2.2 → memplex-3.2.4}/memplex/benchmarks/popqa_hotpot.py +0 -0
  41. {memplex-3.2.2 → memplex-3.2.4}/memplex/config.py +0 -0
  42. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/__init__.py +0 -0
  43. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/associator/__init__.py +0 -0
  44. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/associator/domain_classifier.py +0 -0
  45. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/associator/entity_aligner.py +0 -0
  46. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/associator/ref_linker.py +0 -0
  47. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/associator/term_mapper.py +0 -0
  48. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/dictionaries/__init__.py +0 -0
  49. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/engine.py +0 -0
  50. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/extractors/__init__.py +0 -0
  51. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/extractors/docx.py +0 -0
  52. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/extractors/image.py +0 -0
  53. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/extractors/markdown.py +0 -0
  54. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/extractors/pdf.py +0 -0
  55. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/extractors/vision_mapper.py +0 -0
  56. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/handlers/__init__.py +0 -0
  57. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/handlers/clipboard.py +0 -0
  58. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/handlers/file_handler.py +0 -0
  59. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/handlers/url_handler.py +0 -0
  60. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/hooks/__init__.py +0 -0
  61. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/hooks/collector.py +0 -0
  62. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/hooks/hook_event.py +0 -0
  63. {memplex-3.2.2 → memplex-3.2.4}/memplex/core/hooks/registry.py +0 -0
  64. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/__init__.py +0 -0
  65. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/enhancer.py +0 -0
  66. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/fallback_chain.py +0 -0
  67. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/injection_guard.py +0 -0
  68. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/provider.py +0 -0
  69. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/providers/__init__.py +0 -0
  70. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/providers/anthropic.py +0 -0
  71. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/providers/local.py +0 -0
  72. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/providers/rule_based.py +0 -0
  73. {memplex-3.2.2 → memplex-3.2.4}/memplex/llm/sanitizer.py +0 -0
  74. {memplex-3.2.2 → memplex-3.2.4}/memplex/logging_utils.py +0 -0
  75. {memplex-3.2.2 → memplex-3.2.4}/memplex/metrics.py +0 -0
  76. {memplex-3.2.2 → memplex-3.2.4}/memplex/models/__init__.py +0 -0
  77. {memplex-3.2.2 → memplex-3.2.4}/memplex/models/feedback.py +0 -0
  78. {memplex-3.2.2 → memplex-3.2.4}/memplex/models/graph.py +0 -0
  79. {memplex-3.2.2 → memplex-3.2.4}/memplex/models/memory.py +0 -0
  80. {memplex-3.2.2 → memplex-3.2.4}/memplex/models/misc.py +0 -0
  81. {memplex-3.2.2 → memplex-3.2.4}/memplex/models/paragraph.py +0 -0
  82. {memplex-3.2.2 → memplex-3.2.4}/memplex/models/search.py +0 -0
  83. {memplex-3.2.2 → memplex-3.2.4}/memplex/models/source.py +0 -0
  84. {memplex-3.2.2 → memplex-3.2.4}/memplex/models/task.py +0 -0
  85. {memplex-3.2.2 → memplex-3.2.4}/memplex/processing/__init__.py +0 -0
  86. {memplex-3.2.2 → memplex-3.2.4}/memplex/processing/graph_builder.py +0 -0
  87. {memplex-3.2.2 → memplex-3.2.4}/memplex/processing/merger/__init__.py +0 -0
  88. {memplex-3.2.2 → memplex-3.2.4}/memplex/processing/merger/confidence_calculator.py +0 -0
  89. {memplex-3.2.2 → memplex-3.2.4}/memplex/processing/merger/conflict_resolver.py +0 -0
  90. {memplex-3.2.2 → memplex-3.2.4}/memplex/retrieval/__init__.py +0 -0
  91. {memplex-3.2.2 → memplex-3.2.4}/memplex/retrieval/dedup.py +0 -0
  92. {memplex-3.2.2 → memplex-3.2.4}/memplex/retrieval/embedding.py +0 -0
  93. {memplex-3.2.2 → memplex-3.2.4}/memplex/service.py +0 -0
  94. {memplex-3.2.2 → memplex-3.2.4}/memplex/storage/__init__.py +0 -0
  95. {memplex-3.2.2 → memplex-3.2.4}/memplex/storage/base.py +0 -0
  96. {memplex-3.2.2 → memplex-3.2.4}/memplex/storage/changelog.py +0 -0
  97. {memplex-3.2.2 → memplex-3.2.4}/memplex/storage/feedback.py +0 -0
  98. {memplex-3.2.2 → memplex-3.2.4}/memplex/storage/lite/__init__.py +0 -0
  99. {memplex-3.2.2 → memplex-3.2.4}/memplex/storage/lite/store.py +0 -0
  100. {memplex-3.2.2 → memplex-3.2.4}/memplex/storage/vector.py +0 -0
  101. {memplex-3.2.2 → memplex-3.2.4}/memplex/wiki/__init__.py +0 -0
  102. {memplex-3.2.2 → memplex-3.2.4}/memplex/wiki/community.py +0 -0
  103. {memplex-3.2.2 → memplex-3.2.4}/memplex/wiki/compiler.py +0 -0
  104. {memplex-3.2.2 → memplex-3.2.4}/memplex/wiki/generator.py +0 -0
  105. {memplex-3.2.2 → memplex-3.2.4}/memplex/wiki/search.py +0 -0
  106. {memplex-3.2.2 → memplex-3.2.4}/memplex/worker.py +0 -0
  107. {memplex-3.2.2 → memplex-3.2.4}/memplex.egg-info/SOURCES.txt +0 -0
  108. {memplex-3.2.2 → memplex-3.2.4}/memplex.egg-info/dependency_links.txt +0 -0
  109. {memplex-3.2.2 → memplex-3.2.4}/memplex.egg-info/entry_points.txt +0 -0
  110. {memplex-3.2.2 → memplex-3.2.4}/memplex.egg-info/requires.txt +0 -0
  111. {memplex-3.2.2 → memplex-3.2.4}/memplex.egg-info/top_level.txt +0 -0
  112. {memplex-3.2.2 → memplex-3.2.4}/setup.cfg +0 -0
  113. {memplex-3.2.2 → memplex-3.2.4}/tests/test_associators.py +0 -0
  114. {memplex-3.2.2 → memplex-3.2.4}/tests/test_config.py +0 -0
  115. {memplex-3.2.2 → memplex-3.2.4}/tests/test_core_engine.py +0 -0
  116. {memplex-3.2.2 → memplex-3.2.4}/tests/test_graph_builder.py +0 -0
  117. {memplex-3.2.2 → memplex-3.2.4}/tests/test_llm.py +0 -0
  118. {memplex-3.2.2 → memplex-3.2.4}/tests/test_models.py +0 -0
  119. {memplex-3.2.2 → memplex-3.2.4}/tests/test_service.py +0 -0
  120. {memplex-3.2.2 → memplex-3.2.4}/tests/test_storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memplex
3
- Version: 3.2.2
3
+ Version: 3.2.4
4
4
  Summary: Memplex - Memory Complex: multi-agent knowledge graph memory system with 3-layer retrieval
5
5
  Requires-Python: >=3.11
6
6
  License-File: LICENSE
@@ -13,61 +13,67 @@
13
13
 
14
14
  ## Installation
15
15
 
16
- ### Claude Code Plugin
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:
17
20
 
18
- **Option 1: Local plugin (for development)**
19
21
  ```bash
20
- # Clone and install locally
21
- cd plugin
22
- /plugin install ./plugin
22
+ npx memplex setup
23
23
  ```
24
24
 
25
- **Option 2: Via marketplace**
25
+ Install into a specific local agent:
26
+
26
27
  ```bash
27
- /plugin marketplace add ./marketplace.json
28
- /plugin install memplex
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"
29
32
  ```
30
33
 
31
- **Option 3: pip install**
34
+ Install every supported agent config on this machine:
35
+
32
36
  ```bash
33
- pip install memplex
37
+ npx memplex setup --agent all --project-path "$PWD"
34
38
  ```
35
39
 
36
- ### From Source
40
+ Uninstall:
37
41
 
38
42
  ```bash
39
- git clone https://github.com/articultur/memplex.git
40
- cd memplex
41
- pip install -e .
43
+ npx memplex uninstall --agent all
42
44
  ```
43
45
 
44
- ### Agent One-Command Install
46
+ The npm wrapper creates a persistent Python environment at
47
+ `~/.local/share/memplex/agent-venv`, installs `memplex==3.2.4`, 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`.
45
51
 
46
- For a machine that does not have this repository checked out, install Memplex
47
- into a detected local agent with:
52
+ Python-first users can use a persistent tool install:
48
53
 
49
54
  ```bash
50
- curl -fsSL https://raw.githubusercontent.com/articultur/memplex/main/scripts/install-agent.sh | bash
55
+ uv tool install memplex==3.2.4
56
+ memplex setup
51
57
  ```
52
58
 
53
- By default the script installs Memplex from GitHub into a persistent Python
54
- environment at `~/.local/share/memplex/agent-venv`, detects local Codex, Claude
55
- Code, OpenClaw, and Hermes config directories/commands, then registers Memplex
56
- with each detected agent. It uses `uv` when available and falls back to
57
- `python -m venv` plus `pip`.
59
+ The raw hosted installer remains available for shell-only environments:
58
60
 
59
- Install a specific agent:
61
+ ```bash
62
+ curl -fsSL https://raw.githubusercontent.com/articultur/memplex/main/scripts/install-agent.sh | bash
63
+ ```
64
+
65
+ ### Claude Code Plugin
60
66
 
61
67
  ```bash
62
- curl -fsSL https://raw.githubusercontent.com/articultur/memplex/main/scripts/install-agent.sh | \
63
- bash -s -- --agent hermes --package memplex --project-path "$PWD" --user-id "$USER"
68
+ memplex setup --agent claude-code
64
69
  ```
65
70
 
66
- Install every supported agent config on this machine:
71
+ ### From Source
67
72
 
68
73
  ```bash
69
- curl -fsSL https://raw.githubusercontent.com/articultur/memplex/main/scripts/install-agent.sh | \
70
- bash -s -- --agent all --project-path "$PWD"
74
+ git clone https://github.com/articultur/memplex.git
75
+ cd memplex
76
+ pip install -e .
71
77
  ```
72
78
 
73
79
  Uninstall:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memplex",
3
- "version": "3.2.2",
3
+ "version": "3.2.4",
4
4
  "description": "Multi-agent memory system -- persistent knowledge graph with 3-layer retrieval, compaction, and wiki",
5
5
  "author": {
6
6
  "name": "articultur"
@@ -80,9 +80,33 @@ def uninstall_agent(
80
80
 
81
81
 
82
82
  def _expand_agents(agent: str) -> list[str]:
83
- if (agent or "").strip().lower() == "all":
83
+ requested = (agent or "auto").strip().lower()
84
+ if requested == "all":
84
85
  return ["codex", "claude-code", "openclaw", "hermes"]
85
- return [get_agent_manifest(agent)["name"]]
86
+ if requested == "auto":
87
+ detected = _detect_agents()
88
+ if not detected:
89
+ raise ValueError(
90
+ "No supported local agents detected. Re-run with "
91
+ "--agent codex|claude-code|openclaw|hermes|all."
92
+ )
93
+ return detected
94
+ return [get_agent_manifest(requested)["name"]]
95
+
96
+
97
+ def _detect_agents() -> list[str]:
98
+ detected: list[str] = []
99
+ checks = [
100
+ ("codex", "CODEX_HOME", ".codex", "codex"),
101
+ ("claude-code", "CLAUDE_CONFIG_DIR", ".claude", "claude"),
102
+ ("openclaw", "OPENCLAW_CONFIG_DIR", ".openclaw", "openclaw"),
103
+ ("hermes", "HERMES_CONFIG_DIR", ".hermes", "hermes"),
104
+ ]
105
+ for name, env_name, default_name, command in checks:
106
+ root = Path(os.environ.get(env_name, Path.home() / default_name)).expanduser()
107
+ if root.exists() or shutil.which(command):
108
+ detected.append(name)
109
+ return detected
86
110
 
87
111
 
88
112
  def _install_one(
@@ -140,8 +164,8 @@ def _install_codex(target_dir: str | Path | None, *, dry_run: bool) -> AgentInst
140
164
  "[mcp_servers.memplex]",
141
165
  f'command = "{_python_command()}"',
142
166
  'args = ["-m", "memplex.adapters.mcp_server"]',
143
- 'startup_timeout_sec = 10',
144
- 'tool_timeout_sec = 60',
167
+ "startup_timeout_sec = 10",
168
+ "tool_timeout_sec = 60",
145
169
  MANAGED_END,
146
170
  "",
147
171
  ]
@@ -334,14 +358,10 @@ def _uninstall_openclaw(
334
358
  is_managed_entry = _is_managed_openclaw_entry(memplex_entry)
335
359
  config_changed = False
336
360
  previous_memory_slot = (
337
- memplex_entry.get("config", {})
338
- .get("managed", {})
339
- .get("previousMemorySlot")
361
+ memplex_entry.get("config", {}).get("managed", {}).get("previousMemorySlot")
340
362
  )
341
363
  added_allow_entry = (
342
- memplex_entry.get("config", {})
343
- .get("managed", {})
344
- .get("addedAllowEntry", False)
364
+ memplex_entry.get("config", {}).get("managed", {}).get("addedAllowEntry", False)
345
365
  )
346
366
  if is_managed_entry and slots.get("memory") == "memplex":
347
367
  if previous_memory_slot:
@@ -357,11 +377,7 @@ def _uninstall_openclaw(
357
377
  config_changed = True
358
378
  if config_changed and not dry_run:
359
379
  _write_json(config_path, config)
360
- if (
361
- extension_dir.exists()
362
- and _is_managed_openclaw_extension(extension_dir)
363
- and not dry_run
364
- ):
380
+ if extension_dir.exists() and _is_managed_openclaw_extension(extension_dir) and not dry_run:
365
381
  shutil.rmtree(extension_dir)
366
382
  return AgentInstallResult(
367
383
  agent="openclaw",
@@ -414,7 +430,11 @@ def _install_hermes(
414
430
  agent="hermes",
415
431
  action="install",
416
432
  status="planned" if dry_run else "installed",
417
- files=[str(provider_path), str(plugin_dir / "plugin.yaml"), str(plugin_dir / "__init__.py")],
433
+ files=[
434
+ str(provider_path),
435
+ str(plugin_dir / "plugin.yaml"),
436
+ str(plugin_dir / "__init__.py"),
437
+ ],
418
438
  message="Installed Memplex Hermes memory provider plugin and descriptor.",
419
439
  next_steps=["Restart Hermes and select the memplex memory provider."],
420
440
  )
@@ -605,9 +625,7 @@ def compact_memories(context):
605
625
  )
606
626
 
607
627
 
608
- def _write_hermes_provider_plugin(
609
- plugin_dir: Path, provider_config: dict[str, Any]
610
- ) -> None:
628
+ def _write_hermes_provider_plugin(plugin_dir: Path, provider_config: dict[str, Any]) -> None:
611
629
  plugin_dir.mkdir(parents=True, exist_ok=True)
612
630
  (plugin_dir / "plugin.yaml").write_text(
613
631
  "\n".join(
@@ -925,4 +943,4 @@ def _package_version() -> str:
925
943
  try:
926
944
  return pkg_version("memplex")
927
945
  except Exception:
928
- return "3.2.2"
946
+ return "3.2.4"
@@ -13,9 +13,11 @@ Usage::
13
13
  memplex compact --scope project
14
14
  memplex health
15
15
  memplex stats
16
+ memplex setup # Install into detected local agents
17
+ memplex install --agent codex
18
+ memplex uninstall --agent openclaw
16
19
  memplex agent install --agent all
17
20
  memplex agent uninstall --agent openclaw
18
- memplex setup # Install as Claude Code plugin
19
21
  memplex unsetup # Uninstall Claude Code plugin
20
22
 
21
23
  Global options::
@@ -395,80 +397,25 @@ def _get_marketplace_dir() -> Path:
395
397
 
396
398
 
397
399
  def cmd_setup(args: argparse.Namespace) -> int:
398
- """Install Memplex as a Claude Code plugin."""
399
- if getattr(args, "uninstall", False):
400
- return cmd_unsetup(args)
401
-
402
- market_dir = _get_marketplace_dir()
403
- plugin_target = market_dir / "plugin"
404
-
405
- print("Memplex Plugin Setup")
406
- print("=" * 40)
407
-
408
- # 1. Check Python dependencies
409
- print("\n[1/4] Checking dependencies...")
410
- try:
411
- import numpy # noqa: F401
412
- import yaml # noqa: F401
413
-
414
- print(" Core dependencies: OK")
415
- except ImportError as e:
416
- print(f" Missing dependency: {e}")
417
- print(" Run: pip install memplex[embedding]")
418
- return 1
419
-
420
- # 2. Find and copy plugin directory
421
- print("\n[2/4] Installing plugin files...")
422
- try:
423
- source = _get_plugin_source_dir()
424
- except FileNotFoundError as e:
425
- print(f" Error: {e}")
426
- return 1
427
-
428
- if plugin_target.exists():
429
- shutil.rmtree(plugin_target)
430
-
431
- def _ignore_patterns(_dir, files):
432
- return [f for f in files if f == "__pycache__" or f.endswith(".pyc")]
433
-
434
- shutil.copytree(source, plugin_target, symlinks=False, ignore=_ignore_patterns)
435
- print(f" Installed to: {plugin_target}")
436
-
437
- # 3. Write marketplace.json
438
- print("\n[3/4] Registering with Claude Code...")
439
- market_json = market_dir / "marketplace.json"
440
- market_dir.mkdir(parents=True, exist_ok=True)
441
- market_json.write_text(_MARKETPLACE_JSON.strip() + "\n")
442
- print(f" Marketplace: {market_json}")
443
-
444
- # 4. Write install marker
445
- print("\n[4/4] Writing install marker...")
446
- marker = market_dir / ".install-version"
447
- from importlib.metadata import version as pkg_version
400
+ """Install or uninstall Memplex in local agent hosts."""
401
+ from memplex.adapters.agent_installer import install_agent, uninstall_agent
448
402
 
449
- try:
450
- ver = pkg_version("memplex")
451
- except Exception:
452
- ver = "3.2.2"
453
- marker.write_text(
454
- json.dumps(
455
- {
456
- "version": ver,
457
- "installedAt": __import__("datetime").datetime.now().isoformat(),
458
- },
459
- indent=2,
403
+ should_uninstall = getattr(args, "uninstall", False) or args.command == "uninstall"
404
+ if should_uninstall:
405
+ result = uninstall_agent(
406
+ args.agent,
407
+ target_dir=getattr(args, "target_dir", None),
408
+ dry_run=getattr(args, "dry_run", False),
460
409
  )
461
- )
462
- print(f" Version: {ver}")
463
-
464
- print("\n" + "=" * 40)
465
- print("Memplex plugin installed successfully!")
466
- print("\nWhat was configured:")
467
- print(f" - Hooks: {plugin_target}/hooks/hooks.json")
468
- print(f" - MCP: {plugin_target}/.mcp.json")
469
- print(f" - Skills: {plugin_target}/skills/*/SKILL.md")
470
- print(f" - Manifest: {plugin_target}/../.claude-plugin/plugin.json")
471
- print("\nRestart Claude Code to activate the plugin.")
410
+ else:
411
+ result = install_agent(
412
+ args.agent,
413
+ target_dir=getattr(args, "target_dir", None),
414
+ user_id=getattr(args, "user_id", None),
415
+ project_path=getattr(args, "project_path", None),
416
+ dry_run=getattr(args, "dry_run", False),
417
+ )
418
+ print(_fmt(_dataclass_to_dict(result), args.output))
472
419
  return 0
473
420
 
474
421
 
@@ -579,7 +526,7 @@ def build_parser() -> argparse.ArgumentParser:
579
526
  p_agent_install.add_argument(
580
527
  "--agent",
581
528
  default="all",
582
- help="Agent id: codex | claude-code | openclaw | hermes | all",
529
+ help="Agent id: auto | codex | claude-code | openclaw | hermes | all",
583
530
  )
584
531
  p_agent_install.add_argument(
585
532
  "--target-dir",
@@ -598,7 +545,7 @@ def build_parser() -> argparse.ArgumentParser:
598
545
  p_agent_uninstall.add_argument(
599
546
  "--agent",
600
547
  default="all",
601
- help="Agent id: codex | claude-code | openclaw | hermes | all",
548
+ help="Agent id: auto | codex | claude-code | openclaw | hermes | all",
602
549
  )
603
550
  p_agent_uninstall.add_argument(
604
551
  "--target-dir",
@@ -629,11 +576,39 @@ def build_parser() -> argparse.ArgumentParser:
629
576
  p_agent_capture.add_argument("--assistant-message", required=True)
630
577
  p_agent_capture.add_argument("--next-prompt-hint", default=None)
631
578
 
632
- # -- setup --
633
- p_setup = sub.add_parser("setup", help="Install Memplex as a Claude Code plugin")
634
- p_setup.add_argument(
635
- "--uninstall", action="store_true", help="Uninstall the plugin"
636
- )
579
+ # -- setup / install / uninstall --
580
+ def add_setup_parser(name: str, *, uninstall: bool = False):
581
+ help_text = (
582
+ "Uninstall Memplex from local agent hosts"
583
+ if uninstall
584
+ else "Set up Memplex in detected local agent hosts"
585
+ )
586
+ p_setup = sub.add_parser(name, help=help_text)
587
+ p_setup.add_argument(
588
+ "--agent",
589
+ default="auto",
590
+ help="Agent id: auto | codex | claude-code | openclaw | hermes | all",
591
+ )
592
+ p_setup.add_argument(
593
+ "--target-dir",
594
+ default=None,
595
+ help="Override the selected agent config root directory",
596
+ )
597
+ p_setup.add_argument("--user-id", default=None)
598
+ p_setup.add_argument("--project-path", default=None)
599
+ p_setup.add_argument(
600
+ "--dry-run", action="store_true", help="Show planned files without writing"
601
+ )
602
+ if not uninstall:
603
+ p_setup.add_argument(
604
+ "--uninstall", action="store_true", help="Uninstall instead of install"
605
+ )
606
+ return p_setup
607
+
608
+ add_setup_parser("setup")
609
+ add_setup_parser("install")
610
+ add_setup_parser("stepup")
611
+ add_setup_parser("uninstall", uninstall=True)
637
612
 
638
613
  # -- unsetup --
639
614
  sub.add_parser("unsetup", help="Uninstall Memplex Claude Code plugin")
@@ -671,6 +646,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
671
646
  "stats": cmd_stats,
672
647
  "agent": cmd_agent,
673
648
  "setup": cmd_setup,
649
+ "install": cmd_setup,
650
+ "stepup": cmd_setup,
651
+ "uninstall": cmd_setup,
674
652
  "unsetup": cmd_unsetup,
675
653
  }
676
654
 
@@ -331,7 +331,7 @@ class MCPServer:
331
331
  },
332
332
  "serverInfo": {
333
333
  "name": "memplex",
334
- "version": "3.2.2",
334
+ "version": "3.2.4",
335
335
  },
336
336
  }
337
337
 
@@ -353,9 +353,7 @@ class MCPServer:
353
353
  "content": [
354
354
  {
355
355
  "type": "text",
356
- "text": json.dumps(
357
- result, default=str, ensure_ascii=False, indent=2
358
- ),
356
+ "text": json.dumps(result, default=str, ensure_ascii=False, indent=2),
359
357
  }
360
358
  ],
361
359
  }
@@ -400,9 +398,7 @@ class MCPServer:
400
398
  )
401
399
  return {
402
400
  "total": len(result.results),
403
- "scope": result.scope.value
404
- if hasattr(result.scope, "value")
405
- else str(result.scope),
401
+ "scope": result.scope.value if hasattr(result.scope, "value") else str(result.scope),
406
402
  "latency_ms": result.latency_ms,
407
403
  "results": [
408
404
  {
@@ -29,7 +29,16 @@ import logging
29
29
  import os
30
30
  import time
31
31
  from dataclasses import dataclass
32
- from datetime import datetime
32
+ from datetime import datetime, timezone
33
+
34
+
35
+ def _ensure_aware(dt: datetime) -> datetime:
36
+ """Normalize a datetime to offset-aware UTC for safe arithmetic."""
37
+ if dt.tzinfo is None:
38
+ return dt.replace(tzinfo=timezone.utc)
39
+ return dt
40
+
41
+
33
42
  from pathlib import Path
34
43
  from typing import TYPE_CHECKING, List, Optional
35
44
 
@@ -251,9 +260,7 @@ class CompactionPipeline:
251
260
  skipped=False,
252
261
  )
253
262
 
254
- async def _execute_stage(
255
- self, stage: str, scope: CompactionScope
256
- ) -> CompactionStageResult:
263
+ async def _execute_stage(self, stage: str, scope: CompactionScope) -> CompactionStageResult:
257
264
  """Dispatch to the correct stage handler."""
258
265
  handlers = {
259
266
  "extract": self._execute_extract,
@@ -334,9 +341,7 @@ class CompactionPipeline:
334
341
 
335
342
  # Sort by weight * observation composite score
336
343
  def _score(fv: FieldValue) -> float:
337
- return fv.weight * (
338
- fv.observation if fv.observation is not None else 1.0
339
- )
344
+ return fv.weight * (fv.observation if fv.observation is not None else 1.0)
340
345
 
341
346
  values.sort(key=_score, reverse=True)
342
347
  for fv in values[max_values:]:
@@ -376,7 +381,7 @@ class CompactionPipeline:
376
381
 
377
382
  removed = 0
378
383
  processed = len(functions)
379
- now = datetime.now()
384
+ now = datetime.now(timezone.utc)
380
385
 
381
386
  for func in functions:
382
387
  should_delete = False
@@ -394,7 +399,7 @@ class CompactionPipeline:
394
399
  except (ValueError, TypeError):
395
400
  updated = None
396
401
  if updated is not None:
397
- age_days = (now - updated).days
402
+ age_days = (now - _ensure_aware(updated)).days
398
403
  if age_days > max_age_days and func.access_count < min_access:
399
404
  should_delete = True
400
405
 
@@ -406,7 +411,7 @@ class CompactionPipeline:
406
411
  review_until = datetime.fromisoformat(review_until)
407
412
  except (ValueError, TypeError):
408
413
  review_until = None
409
- if review_until is not None and now > review_until:
414
+ if review_until is not None and now > _ensure_aware(review_until):
410
415
  should_delete = True
411
416
  elif review_until is None:
412
417
  # No expiry set -- use TTL from creation
@@ -416,7 +421,7 @@ class CompactionPipeline:
416
421
  created = datetime.fromisoformat(created)
417
422
  except (ValueError, TypeError):
418
423
  created = None
419
- if created is not None and (now - created).days > review_ttl:
424
+ if created is not None and (now - _ensure_aware(created)).days > review_ttl:
420
425
  should_delete = True
421
426
 
422
427
  # Prune deprecated FieldValue entries (not the whole Function)
@@ -457,7 +462,7 @@ class CompactionPipeline:
457
462
 
458
463
  functions = self._store.list_functions(limit=100000)
459
464
  max_age_days = self._config.compaction.prune_max_age_days
460
- now = datetime.now()
465
+ now = datetime.now(timezone.utc)
461
466
  archived = 0
462
467
 
463
468
  for func in functions:
@@ -471,7 +476,7 @@ class CompactionPipeline:
471
476
  if updated is None:
472
477
  continue
473
478
 
474
- age_days = (now - updated).days
479
+ age_days = (now - _ensure_aware(updated)).days
475
480
  if age_days > max_age_days and func.access_count == 0:
476
481
  # Write to archive
477
482
  archive_file = archive_dir / f"{func.id}.json"
@@ -23,7 +23,15 @@ from __future__ import annotations
23
23
 
24
24
  import logging
25
25
  import math
26
- from datetime import datetime
26
+ from datetime import datetime, timezone
27
+
28
+
29
+ def _ensure_aware(dt: datetime) -> datetime:
30
+ if dt.tzinfo is None:
31
+ return dt.replace(tzinfo=timezone.utc)
32
+ return dt
33
+
34
+
27
35
  from typing import TYPE_CHECKING, Dict, List, Optional
28
36
 
29
37
  from memplex.models import SearchResult, SourceType
@@ -181,7 +189,7 @@ class Reranker:
181
189
  updated_at = datetime.fromisoformat(updated_at)
182
190
  except (ValueError, TypeError):
183
191
  return 0.5
184
- days_since = max(0, (datetime.now() - updated_at).days)
192
+ days_since = max(0, (datetime.now(timezone.utc) - _ensure_aware(updated_at)).days)
185
193
  return min(1.0, math.exp(-days_since / 60))
186
194
 
187
195
  def _source_weight(self, source_type: SourceType) -> float:
@@ -212,7 +220,7 @@ class Reranker:
212
220
  except (ValueError, TypeError):
213
221
  last_accessed = None
214
222
  if last_accessed is not None:
215
- days = max(0, (datetime.now() - last_accessed).days)
223
+ days = max(0, (datetime.now(timezone.utc) - _ensure_aware(last_accessed)).days)
216
224
  recency = min(1.0, math.exp(-days / 60))
217
225
  else:
218
226
  recency = 0.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memplex
3
- Version: 3.2.2
3
+ Version: 3.2.4
4
4
  Summary: Memplex - Memory Complex: multi-agent knowledge graph memory system with 3-layer retrieval
5
5
  Requires-Python: >=3.11
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memplex"
7
- version = "3.2.2"
7
+ version = "3.2.4"
8
8
  description = "Memplex - Memory Complex: multi-agent knowledge graph memory system with 3-layer retrieval"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -51,6 +51,68 @@ def test_codex_hot_path_uses_managed_mcp_block(tmp_path):
51
51
  assert 'args = ["-m", "memplex.adapters.mcp_server"]' in config
52
52
 
53
53
 
54
+ def test_top_level_setup_auto_detects_agent_without_nested_command(tmp_path):
55
+ home = tmp_path / "home"
56
+ (home / ".codex").mkdir(parents=True)
57
+ result = _run_memplex(
58
+ [
59
+ "--output",
60
+ "json",
61
+ "setup",
62
+ "--dry-run",
63
+ "--project-path",
64
+ str(PROJECT_ROOT),
65
+ ],
66
+ env={**os.environ, "HOME": str(home), "PATH": "/usr/bin:/bin"},
67
+ )
68
+
69
+ assert result.returncode == 0, result.stderr
70
+ payload = json.loads(result.stdout)
71
+ assert payload[0]["agent"] == "codex"
72
+ assert payload[0]["action"] == "install"
73
+ assert payload[0]["status"] == "planned"
74
+
75
+
76
+ def test_top_level_install_and_stepup_aliases(tmp_path):
77
+ for command in ("install", "stepup"):
78
+ result = _run_memplex(
79
+ [
80
+ "--output",
81
+ "json",
82
+ command,
83
+ "--agent",
84
+ "codex",
85
+ "--target-dir",
86
+ str(tmp_path / command),
87
+ "--dry-run",
88
+ ]
89
+ )
90
+ assert result.returncode == 0, result.stderr
91
+ payload = json.loads(result.stdout)
92
+ assert payload[0]["agent"] == "codex"
93
+ assert payload[0]["action"] == "install"
94
+
95
+
96
+ def test_top_level_uninstall_alias(tmp_path):
97
+ result = _run_memplex(
98
+ [
99
+ "--output",
100
+ "json",
101
+ "uninstall",
102
+ "--agent",
103
+ "codex",
104
+ "--target-dir",
105
+ str(tmp_path / "codex"),
106
+ "--dry-run",
107
+ ]
108
+ )
109
+
110
+ assert result.returncode == 0, result.stderr
111
+ payload = json.loads(result.stdout)
112
+ assert payload[0]["agent"] == "codex"
113
+ assert payload[0]["action"] == "uninstall"
114
+
115
+
54
116
  def test_mcp_hot_path_initializes_over_stdio(tmp_path):
55
117
  codex_home = tmp_path / "codex"
56
118
  install = _run_memplex(