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 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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ PYTHONPATH="$ROOT_DIR${PYTHONPATH:+:$PYTHONPATH}" exec python3 -m omnimem.cli "$@"
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,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "0.1.0"
@@ -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)}