memorius 0.2.0__tar.gz → 0.3.0__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 (70) hide show
  1. {memorius-0.2.0/memorius.egg-info → memorius-0.3.0}/PKG-INFO +93 -10
  2. {memorius-0.2.0 → memorius-0.3.0}/README.md +92 -9
  3. {memorius-0.2.0 → memorius-0.3.0}/memorius/__init__.py +1 -1
  4. {memorius-0.2.0 → memorius-0.3.0}/memorius/cli/obsidian.py +6 -1
  5. {memorius-0.2.0 → memorius-0.3.0}/memorius/context_inject.py +0 -1
  6. {memorius-0.2.0 → memorius-0.3.0}/memorius/factcheck.py +48 -11
  7. {memorius-0.2.0 → memorius-0.3.0}/memorius/graph.py +26 -8
  8. memorius-0.3.0/memorius/hooks/__init__.py +37 -0
  9. memorius-0.3.0/memorius/hooks/adapters/__init__.py +35 -0
  10. memorius-0.3.0/memorius/hooks/adapters/base.py +3 -0
  11. memorius-0.3.0/memorius/hooks/adapters/claude_code.py +33 -0
  12. memorius-0.3.0/memorius/hooks/adapters/codex.py +32 -0
  13. memorius-0.3.0/memorius/hooks/adapters/gemini.py +30 -0
  14. memorius-0.3.0/memorius/hooks/adapters/generic.py +65 -0
  15. memorius-0.3.0/memorius/hooks/adapters/openclaude.py +42 -0
  16. memorius-0.3.0/memorius/hooks/adapters/openclaw.py +35 -0
  17. memorius-0.3.0/memorius/hooks/adapters/opencode.py +39 -0
  18. memorius-0.3.0/memorius/hooks/adapters/pi.py +48 -0
  19. memorius-0.3.0/memorius/hooks/models.py +58 -0
  20. {memorius-0.2.0 → memorius-0.3.0}/memorius/llm_extract.py +12 -2
  21. {memorius-0.2.0 → memorius-0.3.0}/memorius/mcp_server.py +29 -12
  22. memorius-0.2.0/memorius/vault.py → memorius-0.3.0/memorius/meta_store.py +52 -457
  23. memorius-0.3.0/memorius/models.py +37 -0
  24. memorius-0.3.0/memorius/rest_server.py +323 -0
  25. {memorius-0.2.0 → memorius-0.3.0}/memorius/temporal.py +12 -4
  26. memorius-0.3.0/memorius/utils.py +29 -0
  27. memorius-0.3.0/memorius/validation.py +57 -0
  28. memorius-0.3.0/memorius/vault.py +244 -0
  29. memorius-0.3.0/memorius/vector_store.py +218 -0
  30. {memorius-0.2.0 → memorius-0.3.0/memorius.egg-info}/PKG-INFO +93 -10
  31. {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/SOURCES.txt +19 -1
  32. {memorius-0.2.0 → memorius-0.3.0}/pyproject.toml +1 -1
  33. memorius-0.3.0/tests/test_graph.py +99 -0
  34. {memorius-0.2.0 → memorius-0.3.0}/tests/test_integration.py +13 -1
  35. memorius-0.3.0/tests/test_temporal.py +77 -0
  36. memorius-0.3.0/tests/test_validation.py +75 -0
  37. memorius-0.2.0/memorius/hooks/__init__.py +0 -447
  38. memorius-0.2.0/memorius/rest_server.py +0 -175
  39. memorius-0.2.0/memorius/utils.py +0 -53
  40. {memorius-0.2.0 → memorius-0.3.0}/LICENSE +0 -0
  41. {memorius-0.2.0 → memorius-0.3.0}/MANIFEST.in +0 -0
  42. {memorius-0.2.0 → memorius-0.3.0}/examples/quickstart.py +0 -0
  43. {memorius-0.2.0 → memorius-0.3.0}/hooks/memorius-hook.sh +0 -0
  44. {memorius-0.2.0 → memorius-0.3.0}/manifest.yaml +0 -0
  45. {memorius-0.2.0 → memorius-0.3.0}/memorius/cli/__init__.py +0 -0
  46. {memorius-0.2.0 → memorius-0.3.0}/memorius/cli/main.py +0 -0
  47. {memorius-0.2.0 → memorius-0.3.0}/memorius/config.py +0 -0
  48. {memorius-0.2.0 → memorius-0.3.0}/memorius/consolidation.py +0 -0
  49. {memorius-0.2.0 → memorius-0.3.0}/memorius/data/LICENSE +0 -0
  50. {memorius-0.2.0 → memorius-0.3.0}/memorius/data/__init__.py +0 -0
  51. {memorius-0.2.0 → memorius-0.3.0}/memorius/data/examples/__init__.py +0 -0
  52. {memorius-0.2.0 → memorius-0.3.0}/memorius/data/examples/quickstart.py +0 -0
  53. {memorius-0.2.0 → memorius-0.3.0}/memorius/data/manifest.yaml +0 -0
  54. {memorius-0.2.0 → memorius-0.3.0}/memorius/data/memorius-hook.sh +0 -0
  55. {memorius-0.2.0 → memorius-0.3.0}/memorius/embeddings.py +0 -0
  56. {memorius-0.2.0 → memorius-0.3.0}/memorius/hooks/cli.py +0 -0
  57. {memorius-0.2.0 → memorius-0.3.0}/memorius/hooks/engine.py +0 -0
  58. {memorius-0.2.0 → memorius-0.3.0}/memorius/normalizers/__init__.py +0 -0
  59. {memorius-0.2.0 → memorius-0.3.0}/memorius/normalizers/cli.py +0 -0
  60. {memorius-0.2.0 → memorius-0.3.0}/memorius/plugin_gen/__init__.py +0 -0
  61. {memorius-0.2.0 → memorius-0.3.0}/memorius/plugin_gen/cli.py +0 -0
  62. {memorius-0.2.0 → memorius-0.3.0}/memorius/session.py +0 -0
  63. {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/dependency_links.txt +0 -0
  64. {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/entry_points.txt +0 -0
  65. {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/requires.txt +0 -0
  66. {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/top_level.txt +0 -0
  67. {memorius-0.2.0 → memorius-0.3.0}/setup.cfg +0 -0
  68. {memorius-0.2.0 → memorius-0.3.0}/tests/test_core.py +0 -0
  69. {memorius-0.2.0 → memorius-0.3.0}/tests/test_features.py +0 -0
  70. {memorius-0.2.0 → memorius-0.3.0}/tests/test_polish_regressions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memorius
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Self-contained memory vault for any AI agent — vector search, session diaries, and agent-agnostic hooks.
5
5
  Author-email: dimonapatrick243 <dimona.patrick@gmail.com>
6
6
  License-Expression: MIT
@@ -37,15 +37,36 @@ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
37
37
  Dynamic: license-file
38
38
 
39
39
  # Memorius
40
+ <img width="1584" height="672" alt="memorius" src="https://github.com/user-attachments/assets/1a62163d-1ff5-4112-8b8f-1a6b19afab7a" />
41
+
42
+ <p align="center">
43
+ <strong>Universal memory vault for any AI agent.</strong>
44
+ </p>
45
+
46
+ <p align="center">
47
+ <a href="https://pypi.org/project/memorius/"><img src="https://img.shields.io/pypi/v/memorius?color=blue" alt="PyPI version"></a>
48
+ <a href="https://github.com/Dream-Pixels-Forge/memorius/actions/workflows/test.yml"><img src="https://github.com/Dream-Pixels-Forge/memorius/actions/workflows/test.yml/badge.svg" alt="Tests"></a>
49
+ <a href="https://github.com/Dream-Pixels-Forge/memorius/actions/workflows/publish.yml"><img src="https://github.com/Dream-Pixels-Forge/memorius/actions/workflows/publish.yml/badge.svg" alt="PyPI publish"></a>
50
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
51
+ <a href="https://python.org"><img src="https://img.shields.io/badge/python-3.11%20%7C%203.12-blue" alt="Python 3.11 | 3.12"></a>
52
+ </p>
53
+
54
+ > 🧠 **Works with Claude Code, Codex CLI, Gemini CLI, OpenClaude, OpenCode, Pi, and any MCP-compatible agent.**
55
+ >
56
+ > 🌐 **Landing page:** https://dream-pixels-forge.github.io/memorius/
40
57
 
41
- **Universal memory vault for any AI agent.**
58
+ ## Why Memorius?
42
59
 
43
- > 🌐 **Landing page:** https://dream-pixels-forge.github.io/memorius/
60
+ Most AI memory tools lock you into one agent ecosystem. Memorius is **agent-agnostic by design** — the same memory vault works whether you use Claude Code, Codex CLI, Gemini CLI, or any MCP-compatible agent. No vendor lock-in.
44
61
 
45
- Memorius is a self-contained, agent-agnostic memory system that gives LLMs and AI
46
- agents persistent, searchable memory with a hierarchical knowledge organization.
47
- Drop-in with multi-backend vector storage, pluggable embeddings, built-in MCP + REST
48
- servers, and auto-detecting agent hooks for **7 different AI coding agents**.
62
+ | Feature | Memorius | Others |
63
+ |---------|----------|--------|
64
+ | Agent support | **7 agents** (auto-detected) | Usually 1-3 |
65
+ | Protocol | **Open MCP standard** | Proprietary plugins |
66
+ | Memory hierarchy | Vault → Shelf → Folder → Note | Flat |
67
+ | Obsidian integration | Native import/export | ❌ |
68
+ | Self-hosted | ✅ No cloud dependency | Often SaaS |
69
+ | Open source | ✅ MIT license | Varies |
49
70
 
50
71
  ```bash
51
72
  pip install memorius
@@ -224,6 +245,59 @@ Available MCP tools:
224
245
  | `memorius_diary_write` | Write session diary entry |
225
246
  | `memorius_diary_list` | List diary entries |
226
247
  | `memorius_vault_ls` | Browse vault hierarchy |
248
+ | `memorius_consolidate` | Merge duplicate memories, extract insights |
249
+ | `memorius_extract` | Extract structured memories from conversation (LLM) |
250
+ | `memorius_factcheck` | Fact-check a statement against stored memories |
251
+ | `memorius_context` | Get formatted memory context for injection |
252
+ | `memorius_session_profile` | Build session memory profile for inheritance |
253
+ | `memorius_graph_stats` | Knowledge graph statistics |
254
+ | `memorius_memory_stats` | Memory tracking statistics |
255
+
256
+ ## Agent Skill Installation
257
+
258
+ Memorius ships with a ready-to-use agent skill (`skills/memorius/SKILL.md`) for agents that support the Hermes Agent skill format. The skill provides auto-capture rules, smart context injection, session diary templates, and workflow patterns — so agents can use memorius proactively without being told.
259
+
260
+ ### Skill Structure
261
+
262
+ ```
263
+ skills/
264
+ memorius/
265
+ SKILL.md # Full skill definition (auto-capture, context injection, diary rules)
266
+ README.md # Quick command reference
267
+ .memorius_version # Version tracker
268
+ ```
269
+
270
+ ### Installing for Hermes Agent
271
+
272
+ The skill is designed for [Hermes Agent](https://hermes-agent.nousresearch.com) — copy it into your Hermes skills directory:
273
+
274
+ ```bash
275
+ # Copy the skill
276
+ cp -r skills/memorius ~/.hermes/skills/
277
+
278
+ # Verify it's loaded
279
+ hermes skills list | grep memorius
280
+ ```
281
+
282
+ Once installed, Hermes will automatically detect it and follow the skill's workflows.
283
+
284
+ ### Installing for Other Agents
285
+
286
+ Agents that don't use the Hermes skill format can still use memorius via MCP or CLI:
287
+
288
+ | Agent | Install Command |
289
+ |-------|----------------|
290
+ | Claude Code | `claude mcp add memorius -- memorius serve` |
291
+ | Codex CLI | `codex mcp add memorius -- memorius serve` |
292
+ | Gemini CLI | `gemini mcp add memorius $(which memorius) serve` |
293
+ | Cursor | Add to `.cursor/mcp.json`: `{"mcpServers": {"memorius": {"command": "memorius", "args": ["serve"]}}}` |
294
+ | Aider | `aider --mcp-servers memorius=memorius serve` |
295
+ | Continue | Add memorius to `.continue/config.json` MCP servers |
296
+ | OpenClaw | `openclaw mcp set memorius '{"command":"memorius","args":["serve"]}'` |
297
+
298
+ See `manifest.yaml` for the full list of supported agents and their install commands.
299
+
300
+ Copying the SKILL.md file directly may also work for agents with their own skills directory (e.g. `~/.claude/skills/`, `~/.codex/skills/`) — check your agent's documentation.
227
301
 
228
302
  ## REST API
229
303
 
@@ -234,12 +308,21 @@ memorius serve-rest
234
308
  | Method | Path | Description |
235
309
  |---|---|---|
236
310
  | GET | `/health` | Health check |
311
+ | GET | `/status` | System status |
312
+ | GET | `/stats` | Full vault + memory + graph stats |
237
313
  | POST | `/store` | Store a memory |
238
314
  | POST | `/search` | Semantic search |
239
- | POST | `/mine` | Extract memories |
240
- | GET | `/status` | System status |
315
+ | POST | `/mine` | Extract memories from transcript |
241
316
  | POST | `/diary` | Write diary entry |
242
- | GET | `/palace` | Browse hierarchy |
317
+ | GET | `/vault` | Browse vault hierarchy |
318
+ | GET | `/diaries` | List recent diary entries |
319
+ | POST | `/consolidate` | Merge duplicate memories |
320
+ | POST | `/extract` | Extract memories from conversation (LLM) |
321
+ | POST | `/factcheck` | Fact-check statement against vault |
322
+ | POST | `/context` | Get formatted memory context for injection |
323
+ | GET | `/obsidian` | List notes in Obsidian vault |
324
+ | POST | `/obsidian/import` | Import Obsidian notes as memories |
325
+ | POST | `/obsidian/export` | Export memories as Obsidian notes |
243
326
 
244
327
  ## Agent Hooks
245
328
 
@@ -1,13 +1,34 @@
1
1
  # Memorius
2
+ <img width="1584" height="672" alt="memorius" src="https://github.com/user-attachments/assets/1a62163d-1ff5-4112-8b8f-1a6b19afab7a" />
3
+
4
+ <p align="center">
5
+ <strong>Universal memory vault for any AI agent.</strong>
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://pypi.org/project/memorius/"><img src="https://img.shields.io/pypi/v/memorius?color=blue" alt="PyPI version"></a>
10
+ <a href="https://github.com/Dream-Pixels-Forge/memorius/actions/workflows/test.yml"><img src="https://github.com/Dream-Pixels-Forge/memorius/actions/workflows/test.yml/badge.svg" alt="Tests"></a>
11
+ <a href="https://github.com/Dream-Pixels-Forge/memorius/actions/workflows/publish.yml"><img src="https://github.com/Dream-Pixels-Forge/memorius/actions/workflows/publish.yml/badge.svg" alt="PyPI publish"></a>
12
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
13
+ <a href="https://python.org"><img src="https://img.shields.io/badge/python-3.11%20%7C%203.12-blue" alt="Python 3.11 | 3.12"></a>
14
+ </p>
15
+
16
+ > 🧠 **Works with Claude Code, Codex CLI, Gemini CLI, OpenClaude, OpenCode, Pi, and any MCP-compatible agent.**
17
+ >
18
+ > 🌐 **Landing page:** https://dream-pixels-forge.github.io/memorius/
2
19
 
3
- **Universal memory vault for any AI agent.**
20
+ ## Why Memorius?
4
21
 
5
- > 🌐 **Landing page:** https://dream-pixels-forge.github.io/memorius/
22
+ Most AI memory tools lock you into one agent ecosystem. Memorius is **agent-agnostic by design** — the same memory vault works whether you use Claude Code, Codex CLI, Gemini CLI, or any MCP-compatible agent. No vendor lock-in.
6
23
 
7
- Memorius is a self-contained, agent-agnostic memory system that gives LLMs and AI
8
- agents persistent, searchable memory with a hierarchical knowledge organization.
9
- Drop-in with multi-backend vector storage, pluggable embeddings, built-in MCP + REST
10
- servers, and auto-detecting agent hooks for **7 different AI coding agents**.
24
+ | Feature | Memorius | Others |
25
+ |---------|----------|--------|
26
+ | Agent support | **7 agents** (auto-detected) | Usually 1-3 |
27
+ | Protocol | **Open MCP standard** | Proprietary plugins |
28
+ | Memory hierarchy | Vault → Shelf → Folder → Note | Flat |
29
+ | Obsidian integration | Native import/export | ❌ |
30
+ | Self-hosted | ✅ No cloud dependency | Often SaaS |
31
+ | Open source | ✅ MIT license | Varies |
11
32
 
12
33
  ```bash
13
34
  pip install memorius
@@ -186,6 +207,59 @@ Available MCP tools:
186
207
  | `memorius_diary_write` | Write session diary entry |
187
208
  | `memorius_diary_list` | List diary entries |
188
209
  | `memorius_vault_ls` | Browse vault hierarchy |
210
+ | `memorius_consolidate` | Merge duplicate memories, extract insights |
211
+ | `memorius_extract` | Extract structured memories from conversation (LLM) |
212
+ | `memorius_factcheck` | Fact-check a statement against stored memories |
213
+ | `memorius_context` | Get formatted memory context for injection |
214
+ | `memorius_session_profile` | Build session memory profile for inheritance |
215
+ | `memorius_graph_stats` | Knowledge graph statistics |
216
+ | `memorius_memory_stats` | Memory tracking statistics |
217
+
218
+ ## Agent Skill Installation
219
+
220
+ Memorius ships with a ready-to-use agent skill (`skills/memorius/SKILL.md`) for agents that support the Hermes Agent skill format. The skill provides auto-capture rules, smart context injection, session diary templates, and workflow patterns — so agents can use memorius proactively without being told.
221
+
222
+ ### Skill Structure
223
+
224
+ ```
225
+ skills/
226
+ memorius/
227
+ SKILL.md # Full skill definition (auto-capture, context injection, diary rules)
228
+ README.md # Quick command reference
229
+ .memorius_version # Version tracker
230
+ ```
231
+
232
+ ### Installing for Hermes Agent
233
+
234
+ The skill is designed for [Hermes Agent](https://hermes-agent.nousresearch.com) — copy it into your Hermes skills directory:
235
+
236
+ ```bash
237
+ # Copy the skill
238
+ cp -r skills/memorius ~/.hermes/skills/
239
+
240
+ # Verify it's loaded
241
+ hermes skills list | grep memorius
242
+ ```
243
+
244
+ Once installed, Hermes will automatically detect it and follow the skill's workflows.
245
+
246
+ ### Installing for Other Agents
247
+
248
+ Agents that don't use the Hermes skill format can still use memorius via MCP or CLI:
249
+
250
+ | Agent | Install Command |
251
+ |-------|----------------|
252
+ | Claude Code | `claude mcp add memorius -- memorius serve` |
253
+ | Codex CLI | `codex mcp add memorius -- memorius serve` |
254
+ | Gemini CLI | `gemini mcp add memorius $(which memorius) serve` |
255
+ | Cursor | Add to `.cursor/mcp.json`: `{"mcpServers": {"memorius": {"command": "memorius", "args": ["serve"]}}}` |
256
+ | Aider | `aider --mcp-servers memorius=memorius serve` |
257
+ | Continue | Add memorius to `.continue/config.json` MCP servers |
258
+ | OpenClaw | `openclaw mcp set memorius '{"command":"memorius","args":["serve"]}'` |
259
+
260
+ See `manifest.yaml` for the full list of supported agents and their install commands.
261
+
262
+ Copying the SKILL.md file directly may also work for agents with their own skills directory (e.g. `~/.claude/skills/`, `~/.codex/skills/`) — check your agent's documentation.
189
263
 
190
264
  ## REST API
191
265
 
@@ -196,12 +270,21 @@ memorius serve-rest
196
270
  | Method | Path | Description |
197
271
  |---|---|---|
198
272
  | GET | `/health` | Health check |
273
+ | GET | `/status` | System status |
274
+ | GET | `/stats` | Full vault + memory + graph stats |
199
275
  | POST | `/store` | Store a memory |
200
276
  | POST | `/search` | Semantic search |
201
- | POST | `/mine` | Extract memories |
202
- | GET | `/status` | System status |
277
+ | POST | `/mine` | Extract memories from transcript |
203
278
  | POST | `/diary` | Write diary entry |
204
- | GET | `/palace` | Browse hierarchy |
279
+ | GET | `/vault` | Browse vault hierarchy |
280
+ | GET | `/diaries` | List recent diary entries |
281
+ | POST | `/consolidate` | Merge duplicate memories |
282
+ | POST | `/extract` | Extract memories from conversation (LLM) |
283
+ | POST | `/factcheck` | Fact-check statement against vault |
284
+ | POST | `/context` | Get formatted memory context for injection |
285
+ | GET | `/obsidian` | List notes in Obsidian vault |
286
+ | POST | `/obsidian/import` | Import Obsidian notes as memories |
287
+ | POST | `/obsidian/export` | Export memories as Obsidian notes |
205
288
 
206
289
  ## Agent Hooks
207
290
 
@@ -26,4 +26,4 @@ Architecture:
26
26
  memorius: Main CLI — init, mine, search, diary, status, serve, hook
27
27
  """
28
28
 
29
- __version__ = "0.2.0"
29
+ __version__ = "0.3.0"
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import json
15
15
  import logging
16
+ import os
16
17
  import re
17
18
  from pathlib import Path
18
19
  from typing import Any
@@ -34,7 +35,11 @@ OBSIDIAN_VAULT_HINT = (
34
35
 
35
36
  def _resolve_vault_path(vault_arg: str | None) -> Path:
36
37
  """Resolve the Obsidian vault path from arg, env var, or default."""
37
- raw = vault_arg or Path.home().as_posix() + "/Documents/Obsidian Vault"
38
+ raw = (
39
+ vault_arg
40
+ or os.environ.get("OBSIDIAN_VAULT_PATH")
41
+ or Path.home().as_posix() + "/Documents/Obsidian Vault"
42
+ )
38
43
  path = Path(raw).expanduser().resolve()
39
44
  return path
40
45
 
@@ -12,7 +12,6 @@ from __future__ import annotations
12
12
  import json
13
13
  import logging
14
14
  import re
15
- from datetime import datetime, timezone
16
15
  from typing import Any
17
16
 
18
17
  logger = logging.getLogger("memorius.context")
@@ -14,6 +14,15 @@ from typing import Any
14
14
 
15
15
  logger = logging.getLogger("memorius.factcheck")
16
16
 
17
+ # Confidence scoring constants
18
+ DEFAULT_SIMILARITY_THRESHOLD = 0.7
19
+ CONTRADICTION_CONFIDENCE_PER_ITEM = 0.3
20
+ MAX_CONTRADICTION_CONFIDENCE = 0.9
21
+ VERIFICATION_CONFIDENCE_PER_ITEM = 0.25
22
+ MAX_VERIFICATION_CONFIDENCE = 0.95
23
+ UNCERTAIN_CONFIDENCE = 0.3
24
+ ENTITY_SLOT_MATCH_RATIO = 0.6
25
+
17
26
 
18
27
  @dataclass
19
28
  class FactCheckResult:
@@ -89,7 +98,7 @@ def check_statement(
89
98
  engine,
90
99
  statement: str,
91
100
  vault: str | None = None,
92
- similarity_threshold: float = 0.7,
101
+ similarity_threshold: float = DEFAULT_SIMILARITY_THRESHOLD,
93
102
  ) -> FactCheckResult:
94
103
  """Check a statement against stored memories.
95
104
 
@@ -144,15 +153,15 @@ def check_statement(
144
153
  # Determine verdict
145
154
  if contradicting:
146
155
  verdict = "contradicted"
147
- confidence = min(len(contradicting) * 0.3, 0.9)
156
+ confidence = min(len(contradicting) * CONTRADICTION_CONFIDENCE_PER_ITEM, MAX_CONTRADICTION_CONFIDENCE)
148
157
  explanation = f"Found {len(contradicting)} contradicting memor{'y' if len(contradicting) == 1 else 'ies'}"
149
158
  elif matching:
150
159
  verdict = "verified"
151
- confidence = min(len(matching) * 0.25, 0.95)
160
+ confidence = min(len(matching) * VERIFICATION_CONFIDENCE_PER_ITEM, MAX_VERIFICATION_CONFIDENCE)
152
161
  explanation = f"Found {len(matching)} supporting memor{'y' if len(matching) == 1 else 'ies'}"
153
162
  else:
154
163
  verdict = "uncertain"
155
- confidence = 0.3
164
+ confidence = UNCERTAIN_CONFIDENCE
156
165
  explanation = "Found related memories but none strongly match or contradict"
157
166
 
158
167
  return FactCheckResult(
@@ -220,7 +229,7 @@ def _detect_contradiction(text_a: str, text_b: str) -> bool:
220
229
 
221
230
  # If most words match but a few differ (entity slots), it's a conflict
222
231
  match_ratio = (min_len - len(diff_positions)) / min_len
223
- if match_ratio >= 0.6 and 1 <= len(diff_positions) <= max(1, min_len // 3):
232
+ if match_ratio >= ENTITY_SLOT_MATCH_RATIO and 1 <= len(diff_positions) <= max(1, min_len // 3):
224
233
  # The differing words are potential entity-slot conflicts
225
234
  # Exclude common stop words from being counted as entity differences
226
235
  stop_words = {
@@ -245,17 +254,45 @@ def _detect_contradiction(text_a: str, text_b: str) -> bool:
245
254
 
246
255
 
247
256
  def _text_similarity(text_a: str, text_b: str) -> float:
248
- """Simple text similarity using word overlap (Jaccard)."""
249
- words_a = set(text_a.lower().split())
250
- words_b = set(text_b.lower().split())
257
+ """Text similarity using TF-IDF-like weighted word overlap.
258
+
259
+ Words that are rarer (appear in fewer of the two texts) get higher weight,
260
+ making the similarity more meaningful than plain Jaccard.
261
+ """
262
+ words_a = text_a.lower().split()
263
+ words_b = text_b.lower().split()
251
264
 
252
265
  if not words_a or not words_b:
253
266
  return 0.0
254
267
 
255
- intersection = words_a & words_b
256
- union = words_a | words_b
268
+ set_a = set(words_a)
269
+ set_b = set(words_b)
270
+
271
+ # Term frequency: how often each word appears in its source
272
+ tf_a = {w: words_a.count(w) / len(words_a) for w in set_a}
273
+ tf_b = {w: words_b.count(w) / len(words_b) for w in set_b}
274
+
275
+ # Inverse document frequency: words appearing in both texts get lower weight
276
+ all_words = set_a | set_b
277
+ idf = {}
278
+ for w in all_words:
279
+ in_a = 1 if w in set_a else 0
280
+ in_b = 1 if w in set_b else 0
281
+ # IDF-like: rare in one text = higher weight
282
+ idf[w] = 1.0 / (1.0 + in_a + in_b)
283
+
284
+ # Weighted overlap
285
+ intersection = set_a & set_b
286
+ if not intersection:
287
+ return 0.0
288
+
289
+ weighted_sim = sum(tf_a.get(w, 0) * idf[w] + tf_b.get(w, 0) * idf[w]
290
+ for w in intersection)
291
+ # Normalize by max possible (union)
292
+ max_sim = sum(tf_a.get(w, 0) * idf[w] + tf_b.get(w, 0) * idf[w]
293
+ for w in all_words)
257
294
 
258
- return len(intersection) / len(union) if union else 0.0
295
+ return min(weighted_sim / max_sim, 1.0) if max_sim else 0.0
259
296
 
260
297
 
261
298
  def batch_factcheck(
@@ -12,7 +12,6 @@ Links are created by:
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- import json
16
15
  import sqlite3
17
16
  from dataclasses import dataclass, field
18
17
  from datetime import datetime, timezone
@@ -178,12 +177,13 @@ def auto_link_by_proximity(
178
177
  conn: sqlite3.Connection,
179
178
  memory_id: str,
180
179
  all_memories: list[dict[str, Any]],
181
- threshold: float = 0.75,
180
+ threshold: float = 0.3,
182
181
  max_links: int = 10,
183
182
  ):
184
- """Automatically create links based on embedding proximity.
183
+ """Automatically create links based on content similarity.
185
184
 
186
185
  For a given memory, find the most similar others and link them.
186
+ Uses word overlap (Jaccard) when vectors are not available.
187
187
  """
188
188
  source = None
189
189
  for m in all_memories:
@@ -191,23 +191,41 @@ def auto_link_by_proximity(
191
191
  source = m
192
192
  break
193
193
 
194
- if not source or not source.get("vector"):
194
+ if not source:
195
195
  return
196
196
 
197
+ source_content = (source.get("content") or "").lower()
198
+ if not source_content:
199
+ return
200
+ source_words = set(source_content.split())
201
+
197
202
  scored = []
198
203
  for m in all_memories:
199
204
  if m.get("id") == memory_id:
200
205
  continue
201
- if not m.get("vector"):
202
- continue
203
- sim = _cosine_similarity(source["vector"], m["vector"])
206
+
207
+ # Prefer vector cosine similarity if available
208
+ if source.get("vector") and m.get("vector"):
209
+ sim = _cosine_similarity(source["vector"], m["vector"])
210
+ else:
211
+ # Fall back to word overlap (Jaccard)
212
+ target_content = (m.get("content") or "").lower()
213
+ if not target_content:
214
+ continue
215
+ target_words = set(target_content.split())
216
+ if not source_words or not target_words:
217
+ continue
218
+ intersection = source_words & target_words
219
+ union = source_words | target_words
220
+ sim = len(intersection) / len(union) if union else 0.0
221
+
204
222
  if sim >= threshold:
205
223
  scored.append((m["id"], sim))
206
224
 
207
225
  scored.sort(key=lambda x: x[1], reverse=True)
208
226
 
209
227
  for target_id, sim in scored[:max_links]:
210
- link_memories(conn, memory_id, target_id, weight=sim, relation="related")
228
+ link_memories(conn, memory_id, target_id, weight=round(sim, 4), relation="related")
211
229
 
212
230
 
213
231
  def get_graph_stats(conn: sqlite3.Connection) -> dict[str, Any]:
@@ -0,0 +1,37 @@
1
+ """
2
+ Universal Hook Lifecycle Adapter for Memorius
3
+ =================================================
4
+
5
+ Decouples Memorius hooks from any single AI agent's hook protocol.
6
+
7
+ Adapters are now split into individual files under hooks/adapters/:
8
+ - claude_code.py, codex.py, gemini.py, openclaw.py
9
+ - opencode.py, pi.py, openclaude.py, generic.py
10
+
11
+ This module re-exports everything for backwards compatibility.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ # Re-export models
17
+ from memorius.hooks.models import ( # noqa: F401
18
+ HookEventType,
19
+ HookEvent,
20
+ HookResult,
21
+ BaseAgentAdapter,
22
+ AgentAdapterError,
23
+ )
24
+
25
+ # Re-export adapters and registry
26
+ from memorius.hooks.adapters import ( # noqa: F401
27
+ AGENT_ADAPTERS,
28
+ detect_agent,
29
+ ClaudeCodeAdapter,
30
+ CodexAdapter,
31
+ GeminiCliAdapter,
32
+ OpenClawAdapter,
33
+ OpenCodeAdapter,
34
+ PiAdapter,
35
+ OpenClaudeAdapter,
36
+ GenericAgentAdapter,
37
+ )
@@ -0,0 +1,35 @@
1
+ """Agent adapters subpackage — one adapter per file."""
2
+
3
+ from memorius.hooks.adapters.base import BaseAgentAdapter
4
+ from memorius.hooks.adapters.claude_code import ClaudeCodeAdapter
5
+ from memorius.hooks.adapters.codex import CodexAdapter
6
+ from memorius.hooks.adapters.gemini import GeminiCliAdapter
7
+ from memorius.hooks.adapters.openclaw import OpenClawAdapter
8
+ from memorius.hooks.adapters.opencode import OpenCodeAdapter
9
+ from memorius.hooks.adapters.pi import PiAdapter
10
+ from memorius.hooks.adapters.openclaude import OpenClaudeAdapter
11
+ from memorius.hooks.adapters.generic import GenericAgentAdapter
12
+
13
+ # Registry: ordered by specificity (most specific first)
14
+ AGENT_ADAPTERS: list[type[BaseAgentAdapter]] = [
15
+ OpenClaudeAdapter,
16
+ ClaudeCodeAdapter,
17
+ CodexAdapter,
18
+ GeminiCliAdapter,
19
+ OpenClawAdapter,
20
+ OpenCodeAdapter,
21
+ PiAdapter,
22
+ GenericAgentAdapter, # must be last
23
+ ]
24
+
25
+
26
+ def detect_agent(data: dict) -> type[BaseAgentAdapter]:
27
+ """Auto-detect which agent produced this payload."""
28
+ if isinstance(data, list):
29
+ return GenericAgentAdapter
30
+ for adapter_cls in AGENT_ADAPTERS:
31
+ if adapter_cls is GenericAgentAdapter:
32
+ continue
33
+ if adapter_cls.can_parse(data):
34
+ return adapter_cls
35
+ return GenericAgentAdapter
@@ -0,0 +1,3 @@
1
+ """Base adapter — re-exports from models for convenience."""
2
+
3
+ from memorius.hooks.models import BaseAgentAdapter, AgentAdapterError # noqa: F401
@@ -0,0 +1,33 @@
1
+ """Claude Code adapter."""
2
+
3
+ from memorius.hooks.models import BaseAgentAdapter, HookEvent, HookEventType
4
+
5
+
6
+ class ClaudeCodeAdapter(BaseAgentAdapter):
7
+ """Parses Claude Code's hook protocol v1."""
8
+
9
+ agent_name = "claude-code"
10
+ event_type_map = {
11
+ "stop": HookEventType.SESSION_STOP,
12
+ "precompact": HookEventType.PRE_COMPACT,
13
+ }
14
+
15
+ @classmethod
16
+ def can_parse(cls, data: dict) -> bool:
17
+ has_session = bool(data.get("session_id"))
18
+ has_stop_flag = "stop_hook_active" in data
19
+ has_hook_name = data.get("hook_name", "").lower() in ("stop", "precompact", "save")
20
+ return has_session and (has_stop_flag or has_hook_name)
21
+
22
+ @classmethod
23
+ def parse(cls, data: dict) -> HookEvent:
24
+ raw_type = data.get("hook_name", "stop").lower()
25
+ event_type = cls.event_type_map.get(raw_type, HookEventType.UNKNOWN)
26
+ return HookEvent(
27
+ event_type=event_type,
28
+ session_id=str(data.get("session_id", "unknown")),
29
+ transcript_path=data.get("transcript_path"),
30
+ agent_name=cls.agent_name,
31
+ raw_payload=data,
32
+ can_block=True,
33
+ )
@@ -0,0 +1,32 @@
1
+ """Codex CLI adapter."""
2
+
3
+ from memorius.hooks.models import BaseAgentAdapter, HookEvent, HookEventType
4
+
5
+
6
+ class CodexAdapter(BaseAgentAdapter):
7
+ """Parses Codex CLI's hook protocol."""
8
+
9
+ agent_name = "codex"
10
+ event_type_map = {
11
+ "session-start": HookEventType.SESSION_START,
12
+ "stop": HookEventType.SESSION_STOP,
13
+ "precompact": HookEventType.PRE_COMPACT,
14
+ }
15
+
16
+ @classmethod
17
+ def can_parse(cls, data: dict) -> bool:
18
+ return bool(data.get("session_id")) and "context_dir" in data
19
+
20
+ @classmethod
21
+ def parse(cls, data: dict) -> HookEvent:
22
+ raw_type = data.get("event", "stop").lower()
23
+ event_type = cls.event_type_map.get(raw_type, HookEventType.UNKNOWN)
24
+ return HookEvent(
25
+ event_type=event_type,
26
+ session_id=str(data.get("session_id", "unknown")),
27
+ transcript_path=data.get("transcript_path"),
28
+ project_dir=data.get("context_dir"),
29
+ agent_name=cls.agent_name,
30
+ raw_payload=data,
31
+ can_block=True,
32
+ )
@@ -0,0 +1,30 @@
1
+ """Gemini CLI adapter."""
2
+
3
+ from memorius.hooks.models import BaseAgentAdapter, HookEvent, HookEventType
4
+
5
+
6
+ class GeminiCliAdapter(BaseAgentAdapter):
7
+ """Parses Gemini CLI's PreCompress hook protocol."""
8
+
9
+ agent_name = "gemini-cli"
10
+ event_type_map = {
11
+ "precompress": HookEventType.PRE_COMPACT,
12
+ }
13
+
14
+ @classmethod
15
+ def can_parse(cls, data: dict) -> bool:
16
+ return bool(data.get("session_id")) and "project_id" in data
17
+
18
+ @classmethod
19
+ def parse(cls, data: dict) -> HookEvent:
20
+ raw_type = data.get("event", "precompress").lower()
21
+ event_type = cls.event_type_map.get(raw_type, HookEventType.UNKNOWN)
22
+ return HookEvent(
23
+ event_type=event_type,
24
+ session_id=str(data.get("session_id", "unknown")),
25
+ transcript_path=data.get("transcript_path"),
26
+ project_dir=data.get("project_dir"),
27
+ agent_name=cls.agent_name,
28
+ raw_payload=data,
29
+ can_block=False,
30
+ )