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.
- {memorius-0.2.0/memorius.egg-info → memorius-0.3.0}/PKG-INFO +93 -10
- {memorius-0.2.0 → memorius-0.3.0}/README.md +92 -9
- {memorius-0.2.0 → memorius-0.3.0}/memorius/__init__.py +1 -1
- {memorius-0.2.0 → memorius-0.3.0}/memorius/cli/obsidian.py +6 -1
- {memorius-0.2.0 → memorius-0.3.0}/memorius/context_inject.py +0 -1
- {memorius-0.2.0 → memorius-0.3.0}/memorius/factcheck.py +48 -11
- {memorius-0.2.0 → memorius-0.3.0}/memorius/graph.py +26 -8
- memorius-0.3.0/memorius/hooks/__init__.py +37 -0
- memorius-0.3.0/memorius/hooks/adapters/__init__.py +35 -0
- memorius-0.3.0/memorius/hooks/adapters/base.py +3 -0
- memorius-0.3.0/memorius/hooks/adapters/claude_code.py +33 -0
- memorius-0.3.0/memorius/hooks/adapters/codex.py +32 -0
- memorius-0.3.0/memorius/hooks/adapters/gemini.py +30 -0
- memorius-0.3.0/memorius/hooks/adapters/generic.py +65 -0
- memorius-0.3.0/memorius/hooks/adapters/openclaude.py +42 -0
- memorius-0.3.0/memorius/hooks/adapters/openclaw.py +35 -0
- memorius-0.3.0/memorius/hooks/adapters/opencode.py +39 -0
- memorius-0.3.0/memorius/hooks/adapters/pi.py +48 -0
- memorius-0.3.0/memorius/hooks/models.py +58 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/llm_extract.py +12 -2
- {memorius-0.2.0 → memorius-0.3.0}/memorius/mcp_server.py +29 -12
- memorius-0.2.0/memorius/vault.py → memorius-0.3.0/memorius/meta_store.py +52 -457
- memorius-0.3.0/memorius/models.py +37 -0
- memorius-0.3.0/memorius/rest_server.py +323 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/temporal.py +12 -4
- memorius-0.3.0/memorius/utils.py +29 -0
- memorius-0.3.0/memorius/validation.py +57 -0
- memorius-0.3.0/memorius/vault.py +244 -0
- memorius-0.3.0/memorius/vector_store.py +218 -0
- {memorius-0.2.0 → memorius-0.3.0/memorius.egg-info}/PKG-INFO +93 -10
- {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/SOURCES.txt +19 -1
- {memorius-0.2.0 → memorius-0.3.0}/pyproject.toml +1 -1
- memorius-0.3.0/tests/test_graph.py +99 -0
- {memorius-0.2.0 → memorius-0.3.0}/tests/test_integration.py +13 -1
- memorius-0.3.0/tests/test_temporal.py +77 -0
- memorius-0.3.0/tests/test_validation.py +75 -0
- memorius-0.2.0/memorius/hooks/__init__.py +0 -447
- memorius-0.2.0/memorius/rest_server.py +0 -175
- memorius-0.2.0/memorius/utils.py +0 -53
- {memorius-0.2.0 → memorius-0.3.0}/LICENSE +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/MANIFEST.in +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/examples/quickstart.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/hooks/memorius-hook.sh +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/manifest.yaml +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/cli/__init__.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/cli/main.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/config.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/consolidation.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/data/LICENSE +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/data/__init__.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/data/examples/__init__.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/data/examples/quickstart.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/data/manifest.yaml +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/data/memorius-hook.sh +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/embeddings.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/hooks/cli.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/hooks/engine.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/normalizers/__init__.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/normalizers/cli.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/plugin_gen/__init__.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/plugin_gen/cli.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius/session.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/dependency_links.txt +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/entry_points.txt +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/requires.txt +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/memorius.egg-info/top_level.txt +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/setup.cfg +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/tests/test_core.py +0 -0
- {memorius-0.2.0 → memorius-0.3.0}/tests/test_features.py +0 -0
- {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.
|
|
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
|
-
|
|
58
|
+
## Why Memorius?
|
|
42
59
|
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 | `/
|
|
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
|
-
|
|
20
|
+
## Why Memorius?
|
|
4
21
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 | `/
|
|
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
|
|
|
@@ -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 =
|
|
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
|
|
|
@@ -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 =
|
|
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) *
|
|
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) *
|
|
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 =
|
|
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 >=
|
|
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
|
-
"""
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
|
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.
|
|
180
|
+
threshold: float = 0.3,
|
|
182
181
|
max_links: int = 10,
|
|
183
182
|
):
|
|
184
|
-
"""Automatically create links based on
|
|
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
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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,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
|
+
)
|