rekal 0.0.3__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.
- rekal-0.0.3/.github/workflows/ci.yml +36 -0
- rekal-0.0.3/.github/workflows/release.yml +50 -0
- rekal-0.0.3/.gitignore +53 -0
- rekal-0.0.3/AGENTS.md +170 -0
- rekal-0.0.3/CLAUDE.md +1 -0
- rekal-0.0.3/LICENSE +21 -0
- rekal-0.0.3/PKG-INFO +51 -0
- rekal-0.0.3/README.md +31 -0
- rekal-0.0.3/pyproject.toml +96 -0
- rekal-0.0.3/rekal/__init__.py +5 -0
- rekal-0.0.3/rekal/__main__.py +77 -0
- rekal-0.0.3/rekal/_version.py +24 -0
- rekal-0.0.3/rekal/adapters/__init__.py +0 -0
- rekal-0.0.3/rekal/adapters/mcp_adapter.py +43 -0
- rekal-0.0.3/rekal/adapters/sqlite_adapter.py +745 -0
- rekal-0.0.3/rekal/adapters/tools/__init__.py +3 -0
- rekal-0.0.3/rekal/adapters/tools/conversations.py +69 -0
- rekal-0.0.3/rekal/adapters/tools/core.py +90 -0
- rekal-0.0.3/rekal/adapters/tools/introspection.py +76 -0
- rekal-0.0.3/rekal/adapters/tools/smart_write.py +73 -0
- rekal-0.0.3/rekal/embeddings.py +45 -0
- rekal-0.0.3/rekal/models.py +82 -0
- rekal-0.0.3/rekal/scoring.py +45 -0
- rekal-0.0.3/tests/__init__.py +0 -0
- rekal-0.0.3/tests/conftest.py +43 -0
- rekal-0.0.3/tests/test_cli.py +81 -0
- rekal-0.0.3/tests/test_conversation_tools.py +51 -0
- rekal-0.0.3/tests/test_conversations.py +119 -0
- rekal-0.0.3/tests/test_core_tools.py +56 -0
- rekal-0.0.3/tests/test_embeddings.py +51 -0
- rekal-0.0.3/tests/test_introspection.py +126 -0
- rekal-0.0.3/tests/test_introspection_tools.py +68 -0
- rekal-0.0.3/tests/test_mcp_adapter.py +25 -0
- rekal-0.0.3/tests/test_scoring.py +80 -0
- rekal-0.0.3/tests/test_search.py +72 -0
- rekal-0.0.3/tests/test_smart_write.py +78 -0
- rekal-0.0.3/tests/test_smart_write_tools.py +43 -0
- rekal-0.0.3/tests/test_sqlite_adapter.py +86 -0
- rekal-0.0.3/uv.lock +1194 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v6
|
|
16
|
+
- run: uv sync --frozen
|
|
17
|
+
- run: uv run ruff check rekal/ tests/
|
|
18
|
+
- run: uv run ruff format --check rekal/ tests/
|
|
19
|
+
|
|
20
|
+
typecheck:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
- uses: astral-sh/setup-uv@v6
|
|
25
|
+
- run: uv sync --frozen
|
|
26
|
+
- run: uv run ty check rekal/ tests/
|
|
27
|
+
|
|
28
|
+
test:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
with:
|
|
33
|
+
fetch-depth: 0
|
|
34
|
+
- uses: astral-sh/setup-uv@v6
|
|
35
|
+
- run: uv sync --frozen
|
|
36
|
+
- run: uv run pytest --cov=rekal --cov-report=term-missing --cov-fail-under=100 tests/
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Release to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
ci:
|
|
14
|
+
uses: ./.github/workflows/ci.yml
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
build:
|
|
19
|
+
needs: ci
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
- uses: astral-sh/setup-uv@v6
|
|
26
|
+
- run: uv build
|
|
27
|
+
- uses: actions/upload-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: dist
|
|
30
|
+
path: dist/
|
|
31
|
+
|
|
32
|
+
publish:
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment: pypi
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/download-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/
|
|
41
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
42
|
+
|
|
43
|
+
github-release:
|
|
44
|
+
needs: publish
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
- run: gh release create "${{ github.ref_name }}" --generate-notes
|
|
49
|
+
env:
|
|
50
|
+
GH_TOKEN: ${{ github.token }}
|
rekal-0.0.3/.gitignore
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
sdist/
|
|
10
|
+
wheels/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
|
|
16
|
+
# uv — lockfile is tracked for reproducible CI
|
|
17
|
+
# uv.lock
|
|
18
|
+
|
|
19
|
+
# Testing / coverage
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
htmlcov/
|
|
23
|
+
coverage.xml
|
|
24
|
+
|
|
25
|
+
# Type checkers
|
|
26
|
+
.ty_cache/
|
|
27
|
+
.mypy_cache/
|
|
28
|
+
|
|
29
|
+
# Editors
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
*~
|
|
35
|
+
|
|
36
|
+
# OS
|
|
37
|
+
.DS_Store
|
|
38
|
+
Thumbs.db
|
|
39
|
+
|
|
40
|
+
# rekal database files
|
|
41
|
+
*.db
|
|
42
|
+
*.sqlite
|
|
43
|
+
*.sqlite3
|
|
44
|
+
|
|
45
|
+
# Generated version file
|
|
46
|
+
rekal/_version.py
|
|
47
|
+
|
|
48
|
+
# Claude Code
|
|
49
|
+
.claude/
|
|
50
|
+
|
|
51
|
+
# Environment
|
|
52
|
+
.env
|
|
53
|
+
.env.*
|
rekal-0.0.3/AGENTS.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# rekal — Agent Instructions
|
|
2
|
+
|
|
3
|
+
## What this project is
|
|
4
|
+
|
|
5
|
+
rekal is a Model Context Protocol (MCP) server that gives LLMs persistent long-term memory.
|
|
6
|
+
It uses hybrid search (FTS5 + vector + recency) in a single SQLite file.
|
|
7
|
+
Python 3.14+, installed via `pip install rekal`, runs as a stdio MCP server.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
MCP Client
|
|
13
|
+
│ stdio (JSON-RPC)
|
|
14
|
+
│
|
|
15
|
+
mcp_adapter.py ← FastMCP server, lifespan (creates/closes DB)
|
|
16
|
+
│
|
|
17
|
+
├── tools/core.py ─┐
|
|
18
|
+
├── tools/introspection.py│─ thin @mcp.tool() wrappers, delegate to SqliteDatabase
|
|
19
|
+
├── tools/smart_write.py │
|
|
20
|
+
└── tools/conversations.py┘
|
|
21
|
+
│
|
|
22
|
+
sqlite_adapter.py ← SqliteDatabase @dataclass, ALL SQL lives here
|
|
23
|
+
│
|
|
24
|
+
├── SQLite (memories, conversations, tags, conflicts)
|
|
25
|
+
├── FTS5 (full-text index, auto-synced via triggers)
|
|
26
|
+
└── sqlite-vec (vector index)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Key rules
|
|
30
|
+
|
|
31
|
+
- `SqliteDatabase` is a `@dataclass` holding the `aiosqlite.Connection` and ALL query methods. Every SQL statement lives here. No SQL in tool files.
|
|
32
|
+
- Tool modules in `adapters/tools/` are thin wrappers. They register MCP tools via `@mcp.tool()` and call methods on `SqliteDatabase`.
|
|
33
|
+
- `mcp_adapter.py` creates the FastMCP server, manages lifespan, and imports tool modules so they register.
|
|
34
|
+
- Adding a new tool = add a method to `SqliteDatabase` + add a thin tool wrapper in the appropriate `tools/*.py` file.
|
|
35
|
+
|
|
36
|
+
## Python style — modern, strict, no shortcuts
|
|
37
|
+
|
|
38
|
+
### Never use `Any`
|
|
39
|
+
|
|
40
|
+
Zero tolerance. Every value has a real type. If you reach for `Any`, find the actual type.
|
|
41
|
+
Use `TYPE_CHECKING` imports, union types, `Protocol`, or type aliases instead.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# Correct
|
|
45
|
+
SqlParam = str | int | float | bytes | None
|
|
46
|
+
params: list[SqlParam] = []
|
|
47
|
+
|
|
48
|
+
# Wrong
|
|
49
|
+
params: list[Any] = []
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### No underscore prefixes on attributes or methods
|
|
53
|
+
|
|
54
|
+
Public by default. No `self._db`, no `_helper()`, no `_CONSTANT`.
|
|
55
|
+
Use plain names: `self.db`, `helper()`, `SCHEMA`.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# Correct
|
|
59
|
+
@dataclass
|
|
60
|
+
class SqliteDatabase:
|
|
61
|
+
db: aiosqlite.Connection
|
|
62
|
+
embed: EmbeddingFunc
|
|
63
|
+
|
|
64
|
+
# Wrong
|
|
65
|
+
class SqliteDatabase:
|
|
66
|
+
def __init__(self, db, embed):
|
|
67
|
+
self._db = db
|
|
68
|
+
self._embed = embed
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### No mutable globals
|
|
72
|
+
|
|
73
|
+
No module-level mutable state. Constants like `SCHEMA` (a frozen SQL string) and type aliases like `SqlParam` are fine.
|
|
74
|
+
For values that might look like globals, use functions or dataclass fields instead.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# Correct
|
|
78
|
+
def default_db_path() -> str:
|
|
79
|
+
return str(Path.home() / ".rekal" / "memory.db")
|
|
80
|
+
|
|
81
|
+
# Wrong
|
|
82
|
+
_DEFAULT_DB_PATH = str(Path.home() / ".rekal" / "memory.db")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Use dataclasses everywhere possible
|
|
86
|
+
|
|
87
|
+
Prefer `@dataclass` over hand-written `__init__`. Pydantic `BaseModel` only where serialization is needed (models.py).
|
|
88
|
+
|
|
89
|
+
### Use `Annotated` with `Field(description=...)` on all MCP tool parameters
|
|
90
|
+
|
|
91
|
+
Every tool parameter (except `ctx`) must have a description for the MCP schema.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
@mcp.tool()
|
|
95
|
+
async def memory_store(
|
|
96
|
+
ctx: Context,
|
|
97
|
+
content: Annotated[str, Field(description="The text content to store")],
|
|
98
|
+
limit: Annotated[int, Field(description="Maximum number of results")] = 10,
|
|
99
|
+
) -> str:
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Type annotations on everything
|
|
103
|
+
|
|
104
|
+
Every function parameter, return type, and non-obvious variable. Use `from __future__ import annotations` in every file.
|
|
105
|
+
|
|
106
|
+
### Use union syntax, not Optional
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# Correct
|
|
110
|
+
project: str | None = None
|
|
111
|
+
|
|
112
|
+
# Wrong
|
|
113
|
+
project: Optional[str] = None
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Testing
|
|
117
|
+
|
|
118
|
+
### 100% coverage, no exceptions
|
|
119
|
+
|
|
120
|
+
Every line tested. If truly untestable (e.g., defensive except for impossible conditions), add `# pragma: no cover` with explanation.
|
|
121
|
+
|
|
122
|
+
### No mocking, no monkey patching
|
|
123
|
+
|
|
124
|
+
Use real implementations:
|
|
125
|
+
- Real in-memory SQLite via `:memory:` (sub-millisecond, tests real SQL)
|
|
126
|
+
- Deterministic hash-based embeddings from `conftest.py` (fast, deterministic, correct shape)
|
|
127
|
+
- Real MCP tool functions called with constructed context objects
|
|
128
|
+
|
|
129
|
+
### Test structure
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
tests/
|
|
133
|
+
├── conftest.py # db fixture (in-memory), deterministic_embed
|
|
134
|
+
├── test_sqlite_adapter.py # Direct DB operations
|
|
135
|
+
├── test_search.py # Hybrid search scoring
|
|
136
|
+
├── test_introspection.py # memory_similar, topics, timeline, etc.
|
|
137
|
+
├── test_smart_write.py # supersede, build_context
|
|
138
|
+
├── test_conversations.py # Conversation DAG operations
|
|
139
|
+
├── test_scoring.py # Score normalization math
|
|
140
|
+
├── test_embeddings.py # Embedding utilities
|
|
141
|
+
├── test_core_tools.py # MCP tool wrappers for core
|
|
142
|
+
├── test_introspection_tools.py # MCP tool wrappers for introspection
|
|
143
|
+
├── test_smart_write_tools.py # MCP tool wrappers for smart_write
|
|
144
|
+
├── test_conversation_tools.py # MCP tool wrappers for conversations
|
|
145
|
+
├── test_mcp_adapter.py # Lifespan test
|
|
146
|
+
└── test_cli.py # CLI commands
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## CI checks — all must pass
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
ruff check rekal/ tests/
|
|
153
|
+
ruff format --check rekal/ tests/
|
|
154
|
+
ty check rekal/ tests/
|
|
155
|
+
pytest --cov=rekal --cov-report=term-missing --cov-fail-under=100 tests/
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Tooling config
|
|
159
|
+
|
|
160
|
+
- **ruff** — lint + format, line-length 99, target py314
|
|
161
|
+
- **ty** — type checker, python-version 3.14
|
|
162
|
+
- **pytest** — asyncio_mode auto, 100% coverage required
|
|
163
|
+
|
|
164
|
+
## Dependencies
|
|
165
|
+
|
|
166
|
+
- `mcp[cli]` — FastMCP framework
|
|
167
|
+
- `aiosqlite` — async SQLite
|
|
168
|
+
- `sqlite-vec` — vector search extension
|
|
169
|
+
- `fastembed` — ONNX embeddings (lazy-loaded)
|
|
170
|
+
- `pydantic` — models and validation
|
rekal-0.0.3/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Read AGENTS.md before doing anything.
|
rekal-0.0.3/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jan Bjorge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rekal-0.0.3/PKG-INFO
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rekal
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Long-term memory MCP server for LLMs — hybrid search in a single SQLite file
|
|
5
|
+
Author: Jan Bjorge
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: llm,mcp,memory,sqlite,vector-search
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.14
|
|
14
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
15
|
+
Requires-Dist: fastembed>=0.4.0
|
|
16
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
17
|
+
Requires-Dist: pydantic>=2.0.0
|
|
18
|
+
Requires-Dist: sqlite-vec>=0.1.6
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# rekal
|
|
22
|
+
|
|
23
|
+
Long-term memory MCP server for LLMs. Hybrid search (FTS5 + vector + recency) in a single SQLite file.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install rekal
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Add to your MCP client config:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"mcpServers": {
|
|
38
|
+
"rekal": {
|
|
39
|
+
"command": "rekal"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## CLI
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
rekal serve # Run as MCP server (default)
|
|
49
|
+
rekal health # Show database health report
|
|
50
|
+
rekal export # Export all memories as JSON
|
|
51
|
+
```
|
rekal-0.0.3/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# rekal
|
|
2
|
+
|
|
3
|
+
Long-term memory MCP server for LLMs. Hybrid search (FTS5 + vector + recency) in a single SQLite file.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install rekal
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Add to your MCP client config:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"rekal": {
|
|
19
|
+
"command": "rekal"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## CLI
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
rekal serve # Run as MCP server (default)
|
|
29
|
+
rekal health # Show database health report
|
|
30
|
+
rekal export # Export all memories as JSON
|
|
31
|
+
```
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rekal"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Long-term memory MCP server for LLMs — hybrid search in a single SQLite file"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.14"
|
|
12
|
+
authors = [{ name = "Jan Bjorge" }]
|
|
13
|
+
keywords = ["mcp", "memory", "llm", "sqlite", "vector-search"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"mcp[cli]>=1.0.0",
|
|
22
|
+
"aiosqlite>=0.20.0",
|
|
23
|
+
"sqlite-vec>=0.1.6",
|
|
24
|
+
"fastembed>=0.4.0",
|
|
25
|
+
"pydantic>=2.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
rekal = "rekal.__main__:main"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.version]
|
|
32
|
+
source = "vcs"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.hooks.vcs]
|
|
35
|
+
version-file = "rekal/_version.py"
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
target-version = "py314"
|
|
39
|
+
line-length = 99
|
|
40
|
+
extend-exclude = ["rekal/_version.py"]
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = [
|
|
44
|
+
"E",
|
|
45
|
+
"W",
|
|
46
|
+
"F",
|
|
47
|
+
"I",
|
|
48
|
+
"N",
|
|
49
|
+
"UP",
|
|
50
|
+
"B",
|
|
51
|
+
"A",
|
|
52
|
+
"SIM",
|
|
53
|
+
"TCH",
|
|
54
|
+
"RUF",
|
|
55
|
+
"PTH",
|
|
56
|
+
"RET",
|
|
57
|
+
"ARG",
|
|
58
|
+
"ERA",
|
|
59
|
+
"TID",
|
|
60
|
+
"PL",
|
|
61
|
+
]
|
|
62
|
+
ignore = [
|
|
63
|
+
"PLR0913",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint.per-file-ignores]
|
|
67
|
+
"tests/**" = ["PLR2004", "TCH001", "TCH002", "TCH003", "PLC0415", "ARG001"]
|
|
68
|
+
"rekal/adapters/tools/**" = ["TCH001", "TCH002"]
|
|
69
|
+
"rekal/__main__.py" = ["PLC0415"]
|
|
70
|
+
"rekal/embeddings.py" = ["PLC0415"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.isort]
|
|
73
|
+
known-first-party = ["rekal"]
|
|
74
|
+
|
|
75
|
+
[tool.ty.environment]
|
|
76
|
+
python-version = "3.14"
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
asyncio_mode = "auto"
|
|
80
|
+
|
|
81
|
+
[tool.coverage.run]
|
|
82
|
+
source = ["rekal"]
|
|
83
|
+
|
|
84
|
+
[tool.coverage.report]
|
|
85
|
+
fail_under = 100
|
|
86
|
+
show_missing = true
|
|
87
|
+
omit = ["rekal/_version.py"]
|
|
88
|
+
|
|
89
|
+
[dependency-groups]
|
|
90
|
+
dev = [
|
|
91
|
+
"pytest>=9.0.3",
|
|
92
|
+
"pytest-asyncio>=1.3.0",
|
|
93
|
+
"pytest-cov>=7.1.0",
|
|
94
|
+
"ruff>=0.15.10",
|
|
95
|
+
"ty>=0.0.29",
|
|
96
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""CLI entry point: rekal, rekal health, rekal export."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from rekal.adapters.mcp_adapter import default_db_path
|
|
13
|
+
from rekal.adapters.sqlite_adapter import SqliteDatabase
|
|
14
|
+
from rekal.embeddings import FastEmbedder
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_db_path(args: argparse.Namespace) -> str:
|
|
18
|
+
return args.db or os.environ.get("REKAL_DB_PATH", default_db_path())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def run_serve() -> None: # pragma: no cover — interactive stdio server
|
|
22
|
+
from rekal.adapters.mcp_adapter import mcp
|
|
23
|
+
|
|
24
|
+
await mcp.run_stdio_async()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def run_health(db_path: str) -> None:
|
|
28
|
+
if not Path(db_path).exists():
|
|
29
|
+
print(f"Database not found: {db_path}")
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
embed = FastEmbedder()
|
|
33
|
+
db = await SqliteDatabase.create(db_path, embed)
|
|
34
|
+
try:
|
|
35
|
+
report = await db.memory_health()
|
|
36
|
+
print(json.dumps(report.model_dump(), indent=2))
|
|
37
|
+
finally:
|
|
38
|
+
await db.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def run_export(db_path: str) -> None:
|
|
42
|
+
if not Path(db_path).exists():
|
|
43
|
+
print(f"Database not found: {db_path}")
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
embed = FastEmbedder()
|
|
47
|
+
db = await SqliteDatabase.create(db_path, embed)
|
|
48
|
+
try:
|
|
49
|
+
memories = await db.memory_timeline(limit=100_000)
|
|
50
|
+
data = [m.model_dump() for m in memories]
|
|
51
|
+
print(json.dumps(data, indent=2))
|
|
52
|
+
finally:
|
|
53
|
+
await db.close()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> None:
|
|
57
|
+
parser = argparse.ArgumentParser(prog="rekal", description="rekal memory MCP server")
|
|
58
|
+
parser.add_argument("--db", help="Path to SQLite database file")
|
|
59
|
+
sub = parser.add_subparsers(dest="command")
|
|
60
|
+
|
|
61
|
+
sub.add_parser("serve", help="Run as MCP server (default)")
|
|
62
|
+
sub.add_parser("health", help="Show database health report")
|
|
63
|
+
sub.add_parser("export", help="Export all memories as JSON")
|
|
64
|
+
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
command = args.command or "serve"
|
|
67
|
+
|
|
68
|
+
if command == "serve": # pragma: no cover — interactive stdio server
|
|
69
|
+
asyncio.run(run_serve())
|
|
70
|
+
elif command == "health":
|
|
71
|
+
asyncio.run(run_health(get_db_path(args)))
|
|
72
|
+
elif command == "export":
|
|
73
|
+
asyncio.run(run_export(get_db_path(args)))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__": # pragma: no cover
|
|
77
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.0.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 3)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""FastMCP server, lifespan, and DB initialization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from mcp.server.fastmcp import FastMCP
|
|
12
|
+
|
|
13
|
+
from rekal.adapters.sqlite_adapter import SqliteDatabase
|
|
14
|
+
from rekal.embeddings import FastEmbedder
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import AsyncIterator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def default_db_path() -> str:
|
|
21
|
+
return str(Path.home() / ".rekal" / "memory.db")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AppContext:
|
|
26
|
+
db: SqliteDatabase
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@asynccontextmanager
|
|
30
|
+
async def lifespan(_server: FastMCP) -> AsyncIterator[AppContext]:
|
|
31
|
+
db_path = os.environ.get("REKAL_DB_PATH", default_db_path())
|
|
32
|
+
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
embed = FastEmbedder()
|
|
34
|
+
db = await SqliteDatabase.create(db_path, embed, dimensions=embed.dimensions)
|
|
35
|
+
try:
|
|
36
|
+
yield AppContext(db=db)
|
|
37
|
+
finally:
|
|
38
|
+
await db.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
mcp = FastMCP("rekal", lifespan=lifespan)
|
|
42
|
+
|
|
43
|
+
import rekal.adapters.tools # noqa: E402, F401
|