omnimemory-cli 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 +131 -0
- package/bin/omnimemory +5 -0
- package/db/schema.sql +78 -0
- package/docs/quickstart-10min.md +47 -0
- package/omnimemory/__init__.py +2 -0
- package/omnimemory/adapters.py +123 -0
- package/omnimemory/cli.py +579 -0
- package/omnimemory/core.py +826 -0
- package/omnimemory/webui.py +419 -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 +78 -0
- package/templates/project-minimal/.omni-memory-ignore +8 -0
- package/templates/project-minimal/.omni-memory-session.md +13 -0
- package/templates/project-minimal/.omni-memory.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# OmniMemory
|
|
2
|
+
|
|
3
|
+
OmniMemory 是一个可复用、低耦合、可迁移的 AI 分层记忆基础设施(MVP 起步),目标是让 Claude Code / Codex / Cursor 在跨设备、跨项目、跨账户场景下共享同一记忆体系。
|
|
4
|
+
|
|
5
|
+
当前进度:`Phase D(WebUI 配置与管理)已落地`
|
|
6
|
+
|
|
7
|
+
## 设计目标(与约束)
|
|
8
|
+
|
|
9
|
+
- 跨工具一致:通过“文件协议 + CLI 协议”而不是绑定单一 MCP。
|
|
10
|
+
- 跨设备/跨账户:以 GitHub 私有仓库为主同步层,工具只读写本地协议文件。
|
|
11
|
+
- 低耦合/可拆卸:采用 2-4 文件接入原则,移除时不改业务代码。
|
|
12
|
+
- 多层记忆:即时/短期/长期/归档四层,按重要性/置信度/复用度等信号提升或降级。
|
|
13
|
+
- 人类+机器双视图:Markdown 负责人类可读(核心长期记忆);JSONL + SQLite FTS 负责结构化检索与审计。
|
|
14
|
+
- 安全:不在记忆正文存储密钥,只存凭据引用(credential reference)。
|
|
15
|
+
- 轻依赖:核心默认仅依赖 `bash/sqlite3/git/curl`。
|
|
16
|
+
|
|
17
|
+
## 目录结构
|
|
18
|
+
|
|
19
|
+
- `docs/`:架构、决策记录、接入与流程规范。
|
|
20
|
+
- `spec/`:协议与 JSON Schema。
|
|
21
|
+
- `db/schema.sql`:SQLite 模式(含 FTS)。
|
|
22
|
+
- `scripts/`:安装/卸载/接入/剥离/自检脚本。
|
|
23
|
+
- `omnimemory/`:CLI 实现。
|
|
24
|
+
- `bin/omnimemory`:CLI 入口。
|
|
25
|
+
- `templates/project-minimal/`:任意项目最小接入模板(2-4 文件原则)。
|
|
26
|
+
- `data/`:本地默认存储根(Markdown + JSONL,SQLite 将在 Phase B 初始化)。
|
|
27
|
+
|
|
28
|
+
## 一行命令
|
|
29
|
+
|
|
30
|
+
安装:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bash scripts/install.sh
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
本地 npm 风格启动:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm run start
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
新设备一键 bootstrap(推荐):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bash scripts/bootstrap.sh --repo <你的OmniMemory仓库URL> --remote-url <你的记忆私有仓库URL>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
说明:后续发布到 npm 后可支持 `npx omnimemory-cli` 直接运行。
|
|
49
|
+
|
|
50
|
+
发布后跨设备一键安装(无需复制仓库):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx -y omnimemory-cli
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
接入某项目:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bash scripts/attach_project.sh /path/to/target-project your-project-id
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
从某项目剥离:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bash scripts/detach_project.sh /path/to/target-project
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
卸载:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bash scripts/uninstall.sh
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 快速验证(Phase A)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bash scripts/verify_phase_a.sh
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 快速验证(Phase B)
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bash scripts/verify_phase_b.sh
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 快速验证(Phase C)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
bash scripts/verify_phase_c.sh
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 快速验证(Phase D)
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
bash scripts/verify_phase_d.sh
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## CLI 命令
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
./bin/omnimemory write --summary "example" --body "hello"
|
|
102
|
+
./bin/omnimemory find hello
|
|
103
|
+
./bin/omnimemory checkpoint --summary "cp1"
|
|
104
|
+
./bin/omnimemory brief
|
|
105
|
+
./bin/omnimemory verify
|
|
106
|
+
./bin/omnimemory sync --mode noop
|
|
107
|
+
./bin/omnimemory sync --mode github-push
|
|
108
|
+
./bin/omnimemory adapter cred-resolve --ref env://TOKEN --mask
|
|
109
|
+
./bin/omnimemory adapter notion-write --database-id DB_ID --title "t" --content "c" --dry-run
|
|
110
|
+
./bin/omnimemory adapter r2-put --file ./a.bin --url "https://example.com/presigned" --dry-run
|
|
111
|
+
./bin/omnimemory config-path
|
|
112
|
+
./bin/omnimemory start --host 127.0.0.1 --port 8765
|
|
113
|
+
./bin/omnimemory uninstall --yes
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
详见:`docs/cli-mvp.md`、`docs/phase-c.md`、`docs/webui-config.md`
|
|
117
|
+
详见:`docs/quickstart-10min.md`
|
|
118
|
+
详见:`docs/publish-npm.md`
|
|
119
|
+
|
|
120
|
+
## 阶段闭环流程(强制)
|
|
121
|
+
|
|
122
|
+
每个阶段都按以下闭环执行:
|
|
123
|
+
|
|
124
|
+
1. 阶段总结(目标、完成项、一致性)
|
|
125
|
+
2. 自检(命令与关键输出)
|
|
126
|
+
3. 人工验收步骤
|
|
127
|
+
4. 收集反馈并修正
|
|
128
|
+
5. 记录通过状态
|
|
129
|
+
6. 再进入下一阶段
|
|
130
|
+
|
|
131
|
+
详见:`docs/phase-workflow.md`
|
package/bin/omnimemory
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,47 @@
|
|
|
1
|
+
# 10 分钟上手(跨设备 / 跨项目)
|
|
2
|
+
|
|
3
|
+
## A. 新设备一键安装
|
|
4
|
+
|
|
5
|
+
在任意目录执行:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y omnimemory-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
若你未发布 npm 包,使用仓库脚本替代:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bash scripts/bootstrap.sh --repo <你的OmniMemory仓库URL>
|
|
15
|
+
~/.omnimemory/bin/omnimemory start --host 127.0.0.1 --port 8765
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
打开:`http://127.0.0.1:8765`
|
|
19
|
+
|
|
20
|
+
## B. 新项目接入(不污染业务代码)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
~/.omnimemory/app/scripts/attach_project.sh /path/to/project my-project-id
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
只会新增 3 个文件:
|
|
27
|
+
- `.omni-memory.json`
|
|
28
|
+
- `.omni-memory-session.md`
|
|
29
|
+
- `.omni-memory-ignore`
|
|
30
|
+
|
|
31
|
+
## C. 首次设备同步
|
|
32
|
+
|
|
33
|
+
在 WebUI 点击:`首次接入自动对齐`(pull -> reindex -> push)。
|
|
34
|
+
|
|
35
|
+
之后后台 daemon 会准实时自动同步。
|
|
36
|
+
|
|
37
|
+
## D. 卸载
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
~/.omnimemory/bin/omnimemory uninstall --yes
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
项目剥离:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
~/.omnimemory/bin/omnimemory uninstall --yes --detach-project /path/to/project
|
|
47
|
+
```
|
|
@@ -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)}
|