omnimem 0.1.0
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.
- package/README.md +89 -0
- package/bin/omnimem +5 -0
- package/db/schema.sql +78 -0
- package/docs/quickstart-10min.md +35 -0
- package/omnimem/__init__.py +2 -0
- package/omnimem/adapters.py +123 -0
- package/omnimem/cli.py +579 -0
- package/omnimem/core.py +826 -0
- package/omnimem/webui.py +602 -0
- package/package.json +37 -0
- package/scripts/attach_project.sh +25 -0
- package/scripts/bootstrap.sh +77 -0
- package/scripts/detach_project.sh +21 -0
- package/scripts/install.sh +94 -0
- package/scripts/uninstall.sh +13 -0
- package/scripts/verify_phase_a.sh +52 -0
- package/scripts/verify_phase_b.sh +21 -0
- package/scripts/verify_phase_c.sh +19 -0
- package/scripts/verify_phase_d.sh +28 -0
- package/spec/changelog.md +14 -0
- package/spec/memory-envelope.schema.json +111 -0
- package/spec/memory-event.schema.json +31 -0
- package/spec/protocol.md +77 -0
- package/templates/project-minimal/.omnimem-ignore +8 -0
- package/templates/project-minimal/.omnimem-session.md +13 -0
- package/templates/project-minimal/.omnimem.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# OmniMem
|
|
2
|
+
|
|
3
|
+
OmniMem is reusable, low-coupling memory infrastructure for AI agents across tools, devices, projects, and accounts.
|
|
4
|
+
|
|
5
|
+
Current status: `Phase D implemented (WebUI + auto sync daemon + bootstrap + uninstall)`
|
|
6
|
+
|
|
7
|
+
## Goals
|
|
8
|
+
|
|
9
|
+
- Cross-tool: same behavior via CLI protocol (Claude Code / Codex / Cursor).
|
|
10
|
+
- Cross-device: sync through a private Git repository.
|
|
11
|
+
- Cross-project: reusable memory patterns and references.
|
|
12
|
+
- Low-coupling: attach/remove with minimal project files.
|
|
13
|
+
- Human + machine views: Markdown + JSONL + SQLite FTS.
|
|
14
|
+
- Security: never store secrets in memory body, only credential references.
|
|
15
|
+
|
|
16
|
+
## Structure
|
|
17
|
+
|
|
18
|
+
- `omnimem/`: core CLI and WebUI implementation.
|
|
19
|
+
- `bin/omnimem`: launcher.
|
|
20
|
+
- `scripts/`: install, bootstrap, attach, detach, verify helpers.
|
|
21
|
+
- `templates/project-minimal/`: minimal project integration files.
|
|
22
|
+
- `spec/`: protocol and schemas.
|
|
23
|
+
- `db/schema.sql`: SQLite + FTS schema.
|
|
24
|
+
- `docs/`: architecture and operations docs.
|
|
25
|
+
|
|
26
|
+
## One-command usage
|
|
27
|
+
|
|
28
|
+
Start app (WebUI + daemon):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
~/.omnimem/bin/omnimem
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Optional host/port:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
~/.omnimem/bin/omnimem --host 127.0.0.1 --port 8765
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
Local install from repo:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bash scripts/install.sh
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Bootstrap on a new device:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bash scripts/bootstrap.sh --repo <your-omnimem-repo-url>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Project attach/remove
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
bash scripts/attach_project.sh /path/to/project my-project-id
|
|
58
|
+
bash scripts/detach_project.sh /path/to/project
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Uninstall
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
~/.omnimem/bin/omnimem uninstall --yes
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Publish and npx
|
|
68
|
+
|
|
69
|
+
After publishing to npm, end users can run:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx -y omnimem
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Verification
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bash scripts/verify_phase_a.sh
|
|
79
|
+
bash scripts/verify_phase_b.sh
|
|
80
|
+
bash scripts/verify_phase_c.sh
|
|
81
|
+
bash scripts/verify_phase_d.sh
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Docs
|
|
85
|
+
|
|
86
|
+
- `docs/quickstart-10min.md`
|
|
87
|
+
- `docs/webui-config.md`
|
|
88
|
+
- `docs/publish-npm.md`
|
|
89
|
+
- `docs/install-uninstall.md`
|
package/bin/omnimem
ADDED
package/db/schema.sql
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
PRAGMA foreign_keys = ON;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
schema_version TEXT NOT NULL,
|
|
6
|
+
created_at TEXT NOT NULL,
|
|
7
|
+
updated_at TEXT NOT NULL,
|
|
8
|
+
layer TEXT NOT NULL CHECK (layer IN ('instant', 'short', 'long', 'archive')),
|
|
9
|
+
kind TEXT NOT NULL CHECK (kind IN ('note', 'decision', 'task', 'checkpoint', 'summary', 'evidence')),
|
|
10
|
+
summary TEXT NOT NULL,
|
|
11
|
+
body_md_path TEXT NOT NULL,
|
|
12
|
+
body_text TEXT NOT NULL DEFAULT '',
|
|
13
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
14
|
+
importance_score REAL NOT NULL DEFAULT 0.5 CHECK (importance_score >= 0 AND importance_score <= 1),
|
|
15
|
+
confidence_score REAL NOT NULL DEFAULT 0.5 CHECK (confidence_score >= 0 AND confidence_score <= 1),
|
|
16
|
+
stability_score REAL NOT NULL DEFAULT 0.5 CHECK (stability_score >= 0 AND stability_score <= 1),
|
|
17
|
+
reuse_count INTEGER NOT NULL DEFAULT 0 CHECK (reuse_count >= 0),
|
|
18
|
+
volatility_score REAL NOT NULL DEFAULT 0.5 CHECK (volatility_score >= 0 AND volatility_score <= 1),
|
|
19
|
+
cred_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
20
|
+
source_json TEXT NOT NULL,
|
|
21
|
+
scope_json TEXT NOT NULL,
|
|
22
|
+
integrity_json TEXT NOT NULL
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_memories_layer ON memories(layer);
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_memories_kind ON memories(kind);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance_score);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_memories_reuse_count ON memories(reuse_count);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS memory_refs (
|
|
32
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
memory_id TEXT NOT NULL,
|
|
34
|
+
ref_type TEXT NOT NULL,
|
|
35
|
+
target TEXT NOT NULL,
|
|
36
|
+
note TEXT,
|
|
37
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_memory_refs_memory_id ON memory_refs(memory_id);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_memory_refs_target ON memory_refs(target);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS memory_events (
|
|
44
|
+
event_id TEXT PRIMARY KEY,
|
|
45
|
+
event_type TEXT NOT NULL,
|
|
46
|
+
event_time TEXT NOT NULL,
|
|
47
|
+
memory_id TEXT NOT NULL,
|
|
48
|
+
payload_json TEXT NOT NULL,
|
|
49
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_memory_events_type_time ON memory_events(event_type, event_time);
|
|
53
|
+
|
|
54
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
55
|
+
id UNINDEXED,
|
|
56
|
+
summary,
|
|
57
|
+
body_text,
|
|
58
|
+
tags,
|
|
59
|
+
tokenize = 'unicode61'
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories
|
|
63
|
+
BEGIN
|
|
64
|
+
INSERT INTO memories_fts(id, summary, body_text, tags)
|
|
65
|
+
VALUES (new.id, new.summary, new.body_text, new.tags_json);
|
|
66
|
+
END;
|
|
67
|
+
|
|
68
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories
|
|
69
|
+
BEGIN
|
|
70
|
+
DELETE FROM memories_fts WHERE id = old.id;
|
|
71
|
+
END;
|
|
72
|
+
|
|
73
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories
|
|
74
|
+
BEGIN
|
|
75
|
+
DELETE FROM memories_fts WHERE id = old.id;
|
|
76
|
+
INSERT INTO memories_fts(id, summary, body_text, tags)
|
|
77
|
+
VALUES (new.id, new.summary, new.body_text, new.tags_json);
|
|
78
|
+
END;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Quickstart (10 minutes)
|
|
2
|
+
|
|
3
|
+
## New device
|
|
4
|
+
|
|
5
|
+
After npm publish:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y omnimem
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Without npm publish yet:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bash scripts/bootstrap.sh --repo <your-omnimem-repo-url>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Open UI
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
~/.omnimem/bin/omnimem --host 127.0.0.1 --port 8765
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Open `http://127.0.0.1:8765`.
|
|
24
|
+
|
|
25
|
+
## New project
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
~/.omnimem/app/scripts/attach_project.sh /path/to/project my-project-id
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Remove
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
~/.omnimem/bin/omnimem uninstall --yes
|
|
35
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
NOTION_API_BASE = "https://api.notion.com/v1"
|
|
13
|
+
NOTION_VERSION = "2022-06-28"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve_cred_ref(ref: str) -> str:
|
|
17
|
+
if ref.startswith("env://"):
|
|
18
|
+
key = ref[len("env://") :]
|
|
19
|
+
val = os.getenv(key)
|
|
20
|
+
if val is None:
|
|
21
|
+
raise ValueError(f"environment variable not found: {key}")
|
|
22
|
+
return val
|
|
23
|
+
|
|
24
|
+
if ref.startswith("op://"):
|
|
25
|
+
proc = subprocess.run(["op", "read", ref], check=True, capture_output=True, text=True)
|
|
26
|
+
return proc.stdout.strip()
|
|
27
|
+
|
|
28
|
+
raise ValueError("unsupported cred ref, expected env:// or op://")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _http_json(method: str, url: str, headers: dict[str, str], payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
32
|
+
data = None
|
|
33
|
+
if payload is not None:
|
|
34
|
+
data = json.dumps(payload).encode("utf-8")
|
|
35
|
+
req = urllib.request.Request(url=url, method=method, headers=headers, data=data)
|
|
36
|
+
try:
|
|
37
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
38
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
39
|
+
except urllib.error.HTTPError as exc:
|
|
40
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
41
|
+
raise RuntimeError(f"http {exc.code}: {body}") from exc
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def notion_write_page(
|
|
45
|
+
*,
|
|
46
|
+
token: str,
|
|
47
|
+
database_id: str,
|
|
48
|
+
title: str,
|
|
49
|
+
content: str,
|
|
50
|
+
title_property: str = "Name",
|
|
51
|
+
dry_run: bool = False,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
payload = {
|
|
54
|
+
"parent": {"database_id": database_id},
|
|
55
|
+
"properties": {
|
|
56
|
+
title_property: {
|
|
57
|
+
"title": [{"text": {"content": title}}]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"children": [
|
|
61
|
+
{
|
|
62
|
+
"object": "block",
|
|
63
|
+
"type": "paragraph",
|
|
64
|
+
"paragraph": {
|
|
65
|
+
"rich_text": [{"type": "text", "text": {"content": content[:1900]}}]
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
if dry_run:
|
|
71
|
+
return {"ok": True, "mode": "dry_run", "payload": payload}
|
|
72
|
+
|
|
73
|
+
headers = {
|
|
74
|
+
"Authorization": f"Bearer {token}",
|
|
75
|
+
"Notion-Version": NOTION_VERSION,
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
}
|
|
78
|
+
data = _http_json("POST", f"{NOTION_API_BASE}/pages", headers, payload)
|
|
79
|
+
return {"ok": True, "id": data.get("id"), "url": data.get("url")}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def notion_query_database(
|
|
83
|
+
*,
|
|
84
|
+
token: str,
|
|
85
|
+
database_id: str,
|
|
86
|
+
page_size: int = 5,
|
|
87
|
+
dry_run: bool = False,
|
|
88
|
+
) -> dict[str, Any]:
|
|
89
|
+
payload = {"page_size": page_size}
|
|
90
|
+
if dry_run:
|
|
91
|
+
return {"ok": True, "mode": "dry_run", "payload": payload}
|
|
92
|
+
|
|
93
|
+
headers = {
|
|
94
|
+
"Authorization": f"Bearer {token}",
|
|
95
|
+
"Notion-Version": NOTION_VERSION,
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
}
|
|
98
|
+
data = _http_json("POST", f"{NOTION_API_BASE}/databases/{database_id}/query", headers, payload)
|
|
99
|
+
results = data.get("results", [])
|
|
100
|
+
slim = [{"id": x.get("id"), "url": x.get("url")} for x in results]
|
|
101
|
+
return {"ok": True, "count": len(slim), "items": slim}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def r2_put_presigned(*, file_path: Path, presigned_url: str, dry_run: bool = False) -> dict[str, Any]:
|
|
105
|
+
if dry_run:
|
|
106
|
+
return {"ok": True, "mode": "dry_run", "file": str(file_path), "url_prefix": presigned_url[:40]}
|
|
107
|
+
|
|
108
|
+
proc = subprocess.run(
|
|
109
|
+
["curl", "-sS", "-X", "PUT", "--upload-file", str(file_path), presigned_url],
|
|
110
|
+
check=True,
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
)
|
|
114
|
+
return {"ok": True, "response": proc.stdout.strip()}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def r2_get_presigned(*, presigned_url: str, out_path: Path, dry_run: bool = False) -> dict[str, Any]:
|
|
118
|
+
if dry_run:
|
|
119
|
+
return {"ok": True, "mode": "dry_run", "out": str(out_path), "url_prefix": presigned_url[:40]}
|
|
120
|
+
|
|
121
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
subprocess.run(["curl", "-sS", "-L", "-o", str(out_path), presigned_url], check=True)
|
|
123
|
+
return {"ok": True, "out": str(out_path)}
|