memlink-bridge 0.1.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.
- memlink_bridge-0.1.0/LICENSE +21 -0
- memlink_bridge-0.1.0/PKG-INFO +243 -0
- memlink_bridge-0.1.0/README.md +209 -0
- memlink_bridge-0.1.0/pyproject.toml +103 -0
- memlink_bridge-0.1.0/python/memlink/__init__.py +28 -0
- memlink_bridge-0.1.0/python/memlink/_frontmatter.py +28 -0
- memlink_bridge-0.1.0/python/memlink/cli.py +418 -0
- memlink_bridge-0.1.0/python/memlink/converter.py +510 -0
- memlink_bridge-0.1.0/python/memlink/generic_reader.py +214 -0
- memlink_bridge-0.1.0/python/memlink/models.py +120 -0
- memlink_bridge-0.1.0/python/memlink/ombre_reader.py +184 -0
- memlink_bridge-0.1.0/python/memlink/ombre_writer.py +189 -0
- memlink_bridge-0.1.0/python/memlink/openclaw_reader.py +300 -0
- memlink_bridge-0.1.0/python/memlink/openclaw_writer.py +366 -0
- memlink_bridge-0.1.0/python/memlink/plugin.py +90 -0
- memlink_bridge-0.1.0/python/memlink/py.typed +0 -0
- memlink_bridge-0.1.0/python/memlink/registry.py +114 -0
- memlink_bridge-0.1.0/python/memlink/serialization.py +76 -0
- memlink_bridge-0.1.0/python/memlink/testing.py +144 -0
- memlink_bridge-0.1.0/python/memlink/validators.py +322 -0
- memlink_bridge-0.1.0/python/memlink_bridge.egg-info/PKG-INFO +243 -0
- memlink_bridge-0.1.0/python/memlink_bridge.egg-info/SOURCES.txt +36 -0
- memlink_bridge-0.1.0/python/memlink_bridge.egg-info/dependency_links.txt +1 -0
- memlink_bridge-0.1.0/python/memlink_bridge.egg-info/entry_points.txt +11 -0
- memlink_bridge-0.1.0/python/memlink_bridge.egg-info/requires.txt +10 -0
- memlink_bridge-0.1.0/python/memlink_bridge.egg-info/top_level.txt +1 -0
- memlink_bridge-0.1.0/setup.cfg +4 -0
- memlink_bridge-0.1.0/tests/test_fuzz.py +121 -0
- memlink_bridge-0.1.0/tests/test_generic_reader.py +86 -0
- memlink_bridge-0.1.0/tests/test_models.py +107 -0
- memlink_bridge-0.1.0/tests/test_ombre_reader.py +73 -0
- memlink_bridge-0.1.0/tests/test_ombre_writer.py +170 -0
- memlink_bridge-0.1.0/tests/test_openclaw_reader.py +210 -0
- memlink_bridge-0.1.0/tests/test_openclaw_writer.py +153 -0
- memlink_bridge-0.1.0/tests/test_plugin.py +87 -0
- memlink_bridge-0.1.0/tests/test_schema.py +84 -0
- memlink_bridge-0.1.0/tests/test_serialization.py +81 -0
- memlink_bridge-0.1.0/tests/test_validators.py +67 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 memlink contributors
|
|
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.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memlink-bridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI Memory Interchange Layer — language-neutral bridge for AI memory systems
|
|
5
|
+
Author: memlink contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/velnori/memlink
|
|
8
|
+
Project-URL: Repository, https://github.com/velnori/memlink
|
|
9
|
+
Project-URL: Issues, https://github.com/velnori/memlink/issues
|
|
10
|
+
Keywords: ai,memory,interchange,canonical,bridge,pandoc
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: mkdocs-material>=9; extra == "docs"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# memlink
|
|
36
|
+
|
|
37
|
+
> A language-neutral interchange layer for AI memory systems — similar in spirit to how Pandoc enables document interoperability.
|
|
38
|
+
|
|
39
|
+
[](https://github.com/velnori/memlink/actions/workflows/test.yml)
|
|
40
|
+
[](https://www.python.org/)
|
|
41
|
+
[](https://pypi.org/project/memlink/)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
[](https://github.com/astral-sh/ruff)
|
|
44
|
+
[](https://codecov.io/gh/velnori/memlink)
|
|
45
|
+
|
|
46
|
+
## Why memlink?
|
|
47
|
+
|
|
48
|
+
**Problem**: 10+ AI memory formats (Ombre, OpenClaw, Mem0, Zep, Letta...), each with its own schema. Converting between them naively requires O(n²) converters.
|
|
49
|
+
|
|
50
|
+
**Solution**: A single Canonical Memory intermediate format. Add a new system = write one Reader + one Writer. O(n) complexity.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Ombre ──┐
|
|
54
|
+
Mem0 ──┼──→ Canonical ──┬──→ OpenClaw
|
|
55
|
+
Zep ──┘ └──→ Your Format
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Install from PyPI
|
|
62
|
+
pip install memlink-bridge
|
|
63
|
+
|
|
64
|
+
# Or from source
|
|
65
|
+
git clone https://github.com/velnori/memlink.git
|
|
66
|
+
cd memlink
|
|
67
|
+
pip install -e .
|
|
68
|
+
|
|
69
|
+
# Inspect a memory
|
|
70
|
+
memlink inspect tests/fixtures/ombre_samples/dynamic/user/sample.md
|
|
71
|
+
|
|
72
|
+
# Convert Ombre → OpenClaw
|
|
73
|
+
memlink convert --from ombre --to openclaw \
|
|
74
|
+
-s tests/fixtures/ombre_samples \
|
|
75
|
+
-T ./my-memories/
|
|
76
|
+
|
|
77
|
+
# Validate roundtrip
|
|
78
|
+
memlink validate -s tests/fixtures/ombre_samples --level schema
|
|
79
|
+
|
|
80
|
+
# Show installed formats
|
|
81
|
+
memlink formats
|
|
82
|
+
|
|
83
|
+
# Validate roundtrip integrity
|
|
84
|
+
memlink validate --level roundtrip -s ~/.claude/ombre-buckets/
|
|
85
|
+
|
|
86
|
+
# List installed formats
|
|
87
|
+
memlink formats
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Supported Formats
|
|
91
|
+
|
|
92
|
+
| Plugin | Capability | Covered Apps |
|
|
93
|
+
|--------|-----------|--------------|
|
|
94
|
+
| **Ombre** | Read + Write | [Ombre Brain](https://github.com/P0luz/Ombre-Brain) |
|
|
95
|
+
| **OpenClaw** | Read + Write | OpenClaw core, [basic-memory](https://github.com/basicmachines-co/openclaw-basic-memory), [claw-mem](https://github.com/opensourceclaw/claw-mem) |
|
|
96
|
+
| **Generic** | Read | Obsidian, Logseq, Bear, iA Writer, plain Markdown |
|
|
97
|
+
|
|
98
|
+
**3 plugins, 7+ apps interoperable.** Generic Reader alone covers every app that uses YAML frontmatter + Markdown body — no additional code needed.
|
|
99
|
+
|
|
100
|
+
Add a new format = write one Reader or Writer. Zero changes to core code. That's the O(n) architecture.
|
|
101
|
+
|
|
102
|
+
## Architecture
|
|
103
|
+
|
|
104
|
+
memlink uses a **Canonical Memory** intermediate format. Each AI memory format gets a plugin with three methods:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
class FormatPlugin:
|
|
108
|
+
def read(path) -> ReadResult # Format → Canonical
|
|
109
|
+
def write(memories, path) -> [] # Canonical → Format
|
|
110
|
+
def validate(path) -> [Issue] # Format-specific checks
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Canonical Memory Schema
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
schema_version: "1"
|
|
117
|
+
id: "project-alpha-kickoff"
|
|
118
|
+
name: "Project Alpha Kickoff"
|
|
119
|
+
summary: "Initial planning session"
|
|
120
|
+
body: |
|
|
121
|
+
## Decisions
|
|
122
|
+
- Use TypeScript for frontend
|
|
123
|
+
- Deploy on AWS
|
|
124
|
+
|
|
125
|
+
kind: dynamic # dynamic | permanent | emotion (open string)
|
|
126
|
+
status: active # active | archived
|
|
127
|
+
tags: [meeting, planning]
|
|
128
|
+
domains: [work, project]
|
|
129
|
+
|
|
130
|
+
created_at: "2024-06-28T10:00:00Z"
|
|
131
|
+
updated_at: "2024-06-28T15:30:00Z"
|
|
132
|
+
|
|
133
|
+
importance_score: 0.8
|
|
134
|
+
importance_label: null
|
|
135
|
+
valence: 0.7
|
|
136
|
+
arousal: 0.6
|
|
137
|
+
pinned: false
|
|
138
|
+
checksum: "sha256:abc123..."
|
|
139
|
+
|
|
140
|
+
source:
|
|
141
|
+
format: ombre
|
|
142
|
+
path: dynamic/work/meeting.md
|
|
143
|
+
uri: ombre://dynamic/work/project-alpha-kickoff
|
|
144
|
+
|
|
145
|
+
metadata:
|
|
146
|
+
memlink: # Roundtrip preservation
|
|
147
|
+
source:
|
|
148
|
+
format: ombre
|
|
149
|
+
version: "1.0"
|
|
150
|
+
original:
|
|
151
|
+
importance: 8
|
|
152
|
+
created_tz: "2024-06-28T18:00:00+08:00"
|
|
153
|
+
|
|
154
|
+
extensions: {} # Third-party pass-through
|
|
155
|
+
relationships: [] # v0 reserved
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Full schema spec: [spec/canonical-v1.md](spec/canonical-v1.md) | JSON Schema: [spec/canonical-v1.schema.json](spec/canonical-v1.schema.json)
|
|
159
|
+
|
|
160
|
+
### Feature Loss Report
|
|
161
|
+
|
|
162
|
+
Conversion warns about capabilities mismatch:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
memlink convert --from ombre --to openclaw -s ... -t ...
|
|
166
|
+
|
|
167
|
+
Converted: 128
|
|
168
|
+
|
|
169
|
+
── Feature Loss ──
|
|
170
|
+
relationships 8 dropped
|
|
171
|
+
emotion 14 dropped
|
|
172
|
+
extensions 5 dropped
|
|
173
|
+
─────────────────
|
|
174
|
+
Total loss 27 fields across 19 memories
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## What memlink is NOT
|
|
178
|
+
|
|
179
|
+
- ❌ **Synchronization Engine** — v0 is export/import only
|
|
180
|
+
- ❌ **Memory Database** — Works with files, not live memory APIs
|
|
181
|
+
- ❌ **Embedding Store** — No vector search or similarity
|
|
182
|
+
- ❌ **Knowledge Graph** — No graph traversal or inference
|
|
183
|
+
- ❌ **Not opinionated** — Preserves original data structures, no normalization
|
|
184
|
+
|
|
185
|
+
## Spec Compliance
|
|
186
|
+
|
|
187
|
+
| Canonical v1 feature | Ombre | OpenClaw |
|
|
188
|
+
|---------------------|-------|----------|
|
|
189
|
+
| Core Fields (id, name, body) | ✅ | ✅ |
|
|
190
|
+
| Summary | — | ✅ |
|
|
191
|
+
| Tags & Domains | ✅ | ✅ |
|
|
192
|
+
| Emotion (valence/arousal) | ✅ | — |
|
|
193
|
+
| Importance | ✅ 1-10 | ✅ label/score |
|
|
194
|
+
| Pinned | ✅ | ✅ |
|
|
195
|
+
| Timestamps | ✅ | ✅ |
|
|
196
|
+
| Relationships | ⚠ v0 metadata | ⚠ v0 metadata |
|
|
197
|
+
| Extensions | ⚠ pass-through | ⚠ not preserved |
|
|
198
|
+
|
|
199
|
+
Full spec: [spec/canonical-v1.md](spec/canonical-v1.md)
|
|
200
|
+
|
|
201
|
+
## Non-Goals
|
|
202
|
+
|
|
203
|
+
- ❌ **Not a sync tool** — v0 is export/import only. Bidirectional sync is future work.
|
|
204
|
+
- ❌ **Not a database** — Works with files, not live memory APIs.
|
|
205
|
+
- ❌ **Not opinionated** — Preserves original data structures. No normalization.
|
|
206
|
+
|
|
207
|
+
## Limitations
|
|
208
|
+
|
|
209
|
+
### Lossy Conversions
|
|
210
|
+
|
|
211
|
+
| Direction | What may be lost | Mitigation |
|
|
212
|
+
|-----------|-----------------|------------|
|
|
213
|
+
| OpenClaw → Ombre | Domain if no `metadata.domain` | `--unknown-domain-action default:general` |
|
|
214
|
+
| Ombre → OpenClaw | Local timezone | Stored in `metadata.memlink.original.created_tz` |
|
|
215
|
+
|
|
216
|
+
### Concurrency
|
|
217
|
+
|
|
218
|
+
⚠️ v0 does not support concurrent writes to the same target directory. See [#1](https://github.com/velnori/memlink/issues/1).
|
|
219
|
+
|
|
220
|
+
## Development
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
git clone https://github.com/velnori/memlink.git
|
|
224
|
+
cd memlink
|
|
225
|
+
pip install -e ".[dev]"
|
|
226
|
+
|
|
227
|
+
# Run tests
|
|
228
|
+
pytest tests/ -v
|
|
229
|
+
|
|
230
|
+
# Lint & type check
|
|
231
|
+
ruff check memlink/
|
|
232
|
+
mypy memlink/
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Adding a New Format
|
|
236
|
+
|
|
237
|
+
1. Create `memlink/<format>_reader.py` — implement `FormatPlugin.read()`
|
|
238
|
+
2. Create `memlink/<format>_writer.py` — implement `FormatPlugin.write()`
|
|
239
|
+
3. Register in `pyproject.toml` under `[project.entry-points."memlink.readers"]` and `writers`
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# memlink
|
|
2
|
+
|
|
3
|
+
> A language-neutral interchange layer for AI memory systems — similar in spirit to how Pandoc enables document interoperability.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/velnori/memlink/actions/workflows/test.yml)
|
|
6
|
+
[](https://www.python.org/)
|
|
7
|
+
[](https://pypi.org/project/memlink/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://github.com/astral-sh/ruff)
|
|
10
|
+
[](https://codecov.io/gh/velnori/memlink)
|
|
11
|
+
|
|
12
|
+
## Why memlink?
|
|
13
|
+
|
|
14
|
+
**Problem**: 10+ AI memory formats (Ombre, OpenClaw, Mem0, Zep, Letta...), each with its own schema. Converting between them naively requires O(n²) converters.
|
|
15
|
+
|
|
16
|
+
**Solution**: A single Canonical Memory intermediate format. Add a new system = write one Reader + one Writer. O(n) complexity.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Ombre ──┐
|
|
20
|
+
Mem0 ──┼──→ Canonical ──┬──→ OpenClaw
|
|
21
|
+
Zep ──┘ └──→ Your Format
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Install from PyPI
|
|
28
|
+
pip install memlink-bridge
|
|
29
|
+
|
|
30
|
+
# Or from source
|
|
31
|
+
git clone https://github.com/velnori/memlink.git
|
|
32
|
+
cd memlink
|
|
33
|
+
pip install -e .
|
|
34
|
+
|
|
35
|
+
# Inspect a memory
|
|
36
|
+
memlink inspect tests/fixtures/ombre_samples/dynamic/user/sample.md
|
|
37
|
+
|
|
38
|
+
# Convert Ombre → OpenClaw
|
|
39
|
+
memlink convert --from ombre --to openclaw \
|
|
40
|
+
-s tests/fixtures/ombre_samples \
|
|
41
|
+
-T ./my-memories/
|
|
42
|
+
|
|
43
|
+
# Validate roundtrip
|
|
44
|
+
memlink validate -s tests/fixtures/ombre_samples --level schema
|
|
45
|
+
|
|
46
|
+
# Show installed formats
|
|
47
|
+
memlink formats
|
|
48
|
+
|
|
49
|
+
# Validate roundtrip integrity
|
|
50
|
+
memlink validate --level roundtrip -s ~/.claude/ombre-buckets/
|
|
51
|
+
|
|
52
|
+
# List installed formats
|
|
53
|
+
memlink formats
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Supported Formats
|
|
57
|
+
|
|
58
|
+
| Plugin | Capability | Covered Apps |
|
|
59
|
+
|--------|-----------|--------------|
|
|
60
|
+
| **Ombre** | Read + Write | [Ombre Brain](https://github.com/P0luz/Ombre-Brain) |
|
|
61
|
+
| **OpenClaw** | Read + Write | OpenClaw core, [basic-memory](https://github.com/basicmachines-co/openclaw-basic-memory), [claw-mem](https://github.com/opensourceclaw/claw-mem) |
|
|
62
|
+
| **Generic** | Read | Obsidian, Logseq, Bear, iA Writer, plain Markdown |
|
|
63
|
+
|
|
64
|
+
**3 plugins, 7+ apps interoperable.** Generic Reader alone covers every app that uses YAML frontmatter + Markdown body — no additional code needed.
|
|
65
|
+
|
|
66
|
+
Add a new format = write one Reader or Writer. Zero changes to core code. That's the O(n) architecture.
|
|
67
|
+
|
|
68
|
+
## Architecture
|
|
69
|
+
|
|
70
|
+
memlink uses a **Canonical Memory** intermediate format. Each AI memory format gets a plugin with three methods:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
class FormatPlugin:
|
|
74
|
+
def read(path) -> ReadResult # Format → Canonical
|
|
75
|
+
def write(memories, path) -> [] # Canonical → Format
|
|
76
|
+
def validate(path) -> [Issue] # Format-specific checks
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Canonical Memory Schema
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
schema_version: "1"
|
|
83
|
+
id: "project-alpha-kickoff"
|
|
84
|
+
name: "Project Alpha Kickoff"
|
|
85
|
+
summary: "Initial planning session"
|
|
86
|
+
body: |
|
|
87
|
+
## Decisions
|
|
88
|
+
- Use TypeScript for frontend
|
|
89
|
+
- Deploy on AWS
|
|
90
|
+
|
|
91
|
+
kind: dynamic # dynamic | permanent | emotion (open string)
|
|
92
|
+
status: active # active | archived
|
|
93
|
+
tags: [meeting, planning]
|
|
94
|
+
domains: [work, project]
|
|
95
|
+
|
|
96
|
+
created_at: "2024-06-28T10:00:00Z"
|
|
97
|
+
updated_at: "2024-06-28T15:30:00Z"
|
|
98
|
+
|
|
99
|
+
importance_score: 0.8
|
|
100
|
+
importance_label: null
|
|
101
|
+
valence: 0.7
|
|
102
|
+
arousal: 0.6
|
|
103
|
+
pinned: false
|
|
104
|
+
checksum: "sha256:abc123..."
|
|
105
|
+
|
|
106
|
+
source:
|
|
107
|
+
format: ombre
|
|
108
|
+
path: dynamic/work/meeting.md
|
|
109
|
+
uri: ombre://dynamic/work/project-alpha-kickoff
|
|
110
|
+
|
|
111
|
+
metadata:
|
|
112
|
+
memlink: # Roundtrip preservation
|
|
113
|
+
source:
|
|
114
|
+
format: ombre
|
|
115
|
+
version: "1.0"
|
|
116
|
+
original:
|
|
117
|
+
importance: 8
|
|
118
|
+
created_tz: "2024-06-28T18:00:00+08:00"
|
|
119
|
+
|
|
120
|
+
extensions: {} # Third-party pass-through
|
|
121
|
+
relationships: [] # v0 reserved
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Full schema spec: [spec/canonical-v1.md](spec/canonical-v1.md) | JSON Schema: [spec/canonical-v1.schema.json](spec/canonical-v1.schema.json)
|
|
125
|
+
|
|
126
|
+
### Feature Loss Report
|
|
127
|
+
|
|
128
|
+
Conversion warns about capabilities mismatch:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
memlink convert --from ombre --to openclaw -s ... -t ...
|
|
132
|
+
|
|
133
|
+
Converted: 128
|
|
134
|
+
|
|
135
|
+
── Feature Loss ──
|
|
136
|
+
relationships 8 dropped
|
|
137
|
+
emotion 14 dropped
|
|
138
|
+
extensions 5 dropped
|
|
139
|
+
─────────────────
|
|
140
|
+
Total loss 27 fields across 19 memories
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## What memlink is NOT
|
|
144
|
+
|
|
145
|
+
- ❌ **Synchronization Engine** — v0 is export/import only
|
|
146
|
+
- ❌ **Memory Database** — Works with files, not live memory APIs
|
|
147
|
+
- ❌ **Embedding Store** — No vector search or similarity
|
|
148
|
+
- ❌ **Knowledge Graph** — No graph traversal or inference
|
|
149
|
+
- ❌ **Not opinionated** — Preserves original data structures, no normalization
|
|
150
|
+
|
|
151
|
+
## Spec Compliance
|
|
152
|
+
|
|
153
|
+
| Canonical v1 feature | Ombre | OpenClaw |
|
|
154
|
+
|---------------------|-------|----------|
|
|
155
|
+
| Core Fields (id, name, body) | ✅ | ✅ |
|
|
156
|
+
| Summary | — | ✅ |
|
|
157
|
+
| Tags & Domains | ✅ | ✅ |
|
|
158
|
+
| Emotion (valence/arousal) | ✅ | — |
|
|
159
|
+
| Importance | ✅ 1-10 | ✅ label/score |
|
|
160
|
+
| Pinned | ✅ | ✅ |
|
|
161
|
+
| Timestamps | ✅ | ✅ |
|
|
162
|
+
| Relationships | ⚠ v0 metadata | ⚠ v0 metadata |
|
|
163
|
+
| Extensions | ⚠ pass-through | ⚠ not preserved |
|
|
164
|
+
|
|
165
|
+
Full spec: [spec/canonical-v1.md](spec/canonical-v1.md)
|
|
166
|
+
|
|
167
|
+
## Non-Goals
|
|
168
|
+
|
|
169
|
+
- ❌ **Not a sync tool** — v0 is export/import only. Bidirectional sync is future work.
|
|
170
|
+
- ❌ **Not a database** — Works with files, not live memory APIs.
|
|
171
|
+
- ❌ **Not opinionated** — Preserves original data structures. No normalization.
|
|
172
|
+
|
|
173
|
+
## Limitations
|
|
174
|
+
|
|
175
|
+
### Lossy Conversions
|
|
176
|
+
|
|
177
|
+
| Direction | What may be lost | Mitigation |
|
|
178
|
+
|-----------|-----------------|------------|
|
|
179
|
+
| OpenClaw → Ombre | Domain if no `metadata.domain` | `--unknown-domain-action default:general` |
|
|
180
|
+
| Ombre → OpenClaw | Local timezone | Stored in `metadata.memlink.original.created_tz` |
|
|
181
|
+
|
|
182
|
+
### Concurrency
|
|
183
|
+
|
|
184
|
+
⚠️ v0 does not support concurrent writes to the same target directory. See [#1](https://github.com/velnori/memlink/issues/1).
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
git clone https://github.com/velnori/memlink.git
|
|
190
|
+
cd memlink
|
|
191
|
+
pip install -e ".[dev]"
|
|
192
|
+
|
|
193
|
+
# Run tests
|
|
194
|
+
pytest tests/ -v
|
|
195
|
+
|
|
196
|
+
# Lint & type check
|
|
197
|
+
ruff check memlink/
|
|
198
|
+
mypy memlink/
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Adding a New Format
|
|
202
|
+
|
|
203
|
+
1. Create `memlink/<format>_reader.py` — implement `FormatPlugin.read()`
|
|
204
|
+
2. Create `memlink/<format>_writer.py` — implement `FormatPlugin.write()`
|
|
205
|
+
3. Register in `pyproject.toml` under `[project.entry-points."memlink.readers"]` and `writers`
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "memlink-bridge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AI Memory Interchange Layer — language-neutral bridge for AI memory systems"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "memlink contributors"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["ai", "memory", "interchange", "canonical", "bridge", "pandoc"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Topic :: Utilities",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"pyyaml>=6.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/velnori/memlink"
|
|
35
|
+
Repository = "https://github.com/velnori/memlink"
|
|
36
|
+
Issues = "https://github.com/velnori/memlink/issues"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
memlink = "memlink.cli:main"
|
|
40
|
+
|
|
41
|
+
[project.entry-points."memlink.readers"]
|
|
42
|
+
ombre = "memlink.ombre_reader:OmbreReader"
|
|
43
|
+
openclaw = "memlink.openclaw_reader:OpenClawReader"
|
|
44
|
+
generic = "memlink.generic_reader:GenericReader"
|
|
45
|
+
|
|
46
|
+
[project.entry-points."memlink.writers"]
|
|
47
|
+
ombre = "memlink.ombre_writer:OmbreWriter"
|
|
48
|
+
openclaw = "memlink.openclaw_writer:OpenClawWriter"
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
dev = [
|
|
52
|
+
"pytest>=8",
|
|
53
|
+
"pytest-cov>=5",
|
|
54
|
+
"mypy>=1",
|
|
55
|
+
"ruff>=0.6",
|
|
56
|
+
]
|
|
57
|
+
docs = [
|
|
58
|
+
"mkdocs-material>=9",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
target-version = "py310"
|
|
63
|
+
line-length = 100
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint]
|
|
66
|
+
select = ["E", "F", "I", "N", "W", "UP", "B", "C4", "SIM"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.isort]
|
|
69
|
+
known-first-party = ["memlink"]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.format]
|
|
72
|
+
quote-style = "double"
|
|
73
|
+
indent-style = "space"
|
|
74
|
+
skip-magic-trailing-comma = false
|
|
75
|
+
|
|
76
|
+
[tool.mypy]
|
|
77
|
+
strict = true
|
|
78
|
+
python_version = "3.10"
|
|
79
|
+
ignore_missing_imports = true
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
testpaths = ["tests"]
|
|
83
|
+
python_files = ["test_*.py"]
|
|
84
|
+
pythonpath = ["python"]
|
|
85
|
+
addopts = "-v --tb=short --strict-markers"
|
|
86
|
+
markers = [
|
|
87
|
+
"slow: slow tests (fuzz, performance)",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
[tool.coverage.run]
|
|
91
|
+
source = ["memlink"]
|
|
92
|
+
omit = ["tests/*"]
|
|
93
|
+
|
|
94
|
+
[tool.coverage.report]
|
|
95
|
+
fail_under = 85
|
|
96
|
+
exclude_lines = [
|
|
97
|
+
"pragma: no cover",
|
|
98
|
+
"if TYPE_CHECKING:",
|
|
99
|
+
"raise NotImplementedError",
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
[tool.setuptools.packages.find]
|
|
103
|
+
where = ["python"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""memlink — AI Memory Interchange Layer."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .models import JSONValue, Memory, Relationship, Source
|
|
6
|
+
from .plugin import Capabilities, FormatPlugin, ReadResult, Severity, ValidationIssue
|
|
7
|
+
from .converter import ConversionAnalysis, FeatureImpact, analyze_conversion, compare_memories
|
|
8
|
+
from .registry import get_reader, get_writer, list_formats
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"__version__",
|
|
12
|
+
"Memory",
|
|
13
|
+
"Source",
|
|
14
|
+
"Relationship",
|
|
15
|
+
"JSONValue",
|
|
16
|
+
"FormatPlugin",
|
|
17
|
+
"Capabilities",
|
|
18
|
+
"ReadResult",
|
|
19
|
+
"ValidationIssue",
|
|
20
|
+
"Severity",
|
|
21
|
+
"ConversionAnalysis",
|
|
22
|
+
"FeatureImpact",
|
|
23
|
+
"analyze_conversion",
|
|
24
|
+
"compare_memories",
|
|
25
|
+
"get_reader",
|
|
26
|
+
"get_writer",
|
|
27
|
+
"list_formats",
|
|
28
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Shared YAML frontmatter parser — used by all Readers and validators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_frontmatter(text: str) -> tuple[dict, str]:
|
|
9
|
+
"""Parse YAML frontmatter from a Markdown file.
|
|
10
|
+
|
|
11
|
+
Returns (frontmatter_dict, body_text).
|
|
12
|
+
If no frontmatter found, returns ({}, text).
|
|
13
|
+
If YAML is invalid, returns ({}, text) — never raises.
|
|
14
|
+
"""
|
|
15
|
+
if not text.startswith("---"):
|
|
16
|
+
return {}, text
|
|
17
|
+
|
|
18
|
+
parts = text.split("---", 2)
|
|
19
|
+
if len(parts) < 3:
|
|
20
|
+
return {}, text
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
fm = yaml.safe_load(parts[1])
|
|
24
|
+
if not isinstance(fm, dict):
|
|
25
|
+
return {}, text
|
|
26
|
+
return fm, parts[2]
|
|
27
|
+
except yaml.YAMLError:
|
|
28
|
+
return {}, text
|