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
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sqlite3
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import parse_qs, urlparse
|
|
11
|
+
|
|
12
|
+
from .core import ensure_storage, find_memories, resolve_paths, save_config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
HTML_PAGE = """<!doctype html>
|
|
16
|
+
<html lang="zh-CN">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20
|
+
<title>OmniMemory WebUI</title>
|
|
21
|
+
<style>
|
|
22
|
+
:root { --bg:#f3f4f6; --card:#ffffff; --ink:#0f172a; --muted:#475569; --line:#e2e8f0; --accent:#0f766e; --tab:#e6fffb; }
|
|
23
|
+
body { margin:0; font-family: 'IBM Plex Sans', 'Helvetica Neue', sans-serif; background: radial-gradient(circle at top right,#ecfeff,#f8fafc 50%,#f3f4f6); color:var(--ink); }
|
|
24
|
+
.wrap { max-width: 1080px; margin: 22px auto; padding: 0 16px 36px; }
|
|
25
|
+
.hero { padding: 18px; border:1px solid var(--line); background:var(--card); border-radius: 14px; }
|
|
26
|
+
h1 { margin: 0 0 6px; font-size: 28px; letter-spacing: .2px; }
|
|
27
|
+
.small { font-size:12px; color:var(--muted); }
|
|
28
|
+
.tabs { display:flex; gap:8px; margin-top:14px; flex-wrap:wrap; }
|
|
29
|
+
.tab-btn { border:1px solid var(--line); background:#fff; color:#0f172a; border-radius: 10px; padding:8px 12px; cursor:pointer; }
|
|
30
|
+
.tab-btn.active { background:var(--tab); border-color:#99f6e4; color:#115e59; }
|
|
31
|
+
.panel { display:none; margin-top:14px; }
|
|
32
|
+
.panel.active { display:block; }
|
|
33
|
+
.grid { display:grid; grid-template-columns: 1fr 1fr; gap:16px; }
|
|
34
|
+
.card { border:1px solid var(--line); background:var(--card); border-radius: 14px; padding:16px; box-shadow: 0 4px 16px rgba(15,23,42,.03); }
|
|
35
|
+
.wide { grid-column: 1 / -1; }
|
|
36
|
+
label { display:block; font-size:12px; margin-top:8px; color:var(--muted); }
|
|
37
|
+
input { width:100%; box-sizing:border-box; border:1px solid #cbd5e1; background:#fff; border-radius:10px; padding:9px 10px; margin-top:4px; }
|
|
38
|
+
button { border:0; background:var(--accent); color:#fff; border-radius:10px; padding:10px 14px; margin-top:10px; cursor:pointer; }
|
|
39
|
+
.row-btn { display:flex; gap:10px; flex-wrap:wrap; }
|
|
40
|
+
table { width:100%; border-collapse: collapse; font-size: 14px; }
|
|
41
|
+
th, td { padding:8px; border-bottom:1px solid var(--line); text-align:left; }
|
|
42
|
+
.ok { color:#047857; }
|
|
43
|
+
.err { color:#b91c1c; }
|
|
44
|
+
.warn { color:#92400e; }
|
|
45
|
+
@media (max-width: 920px) { .grid { grid-template-columns:1fr; } }
|
|
46
|
+
</style>
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
<div class="wrap">
|
|
50
|
+
<div class="hero">
|
|
51
|
+
<h1>OmniMemory WebUI</h1>
|
|
52
|
+
<div class="small">简洁模式:状态与动作 / 配置 / 记忆管理</div>
|
|
53
|
+
<div id="status" class="small"></div>
|
|
54
|
+
<div id="daemonState" class="small"></div>
|
|
55
|
+
<div class="tabs">
|
|
56
|
+
<button class="tab-btn active" data-tab="statusTab">状态与动作</button>
|
|
57
|
+
<button class="tab-btn" data-tab="configTab">配置</button>
|
|
58
|
+
<button class="tab-btn" data-tab="memoryTab">记忆管理</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div id="statusTab" class="panel active">
|
|
62
|
+
<div class="grid">
|
|
63
|
+
<div class="card">
|
|
64
|
+
<h3>系统状态</h3>
|
|
65
|
+
<div id="initState" class="small"></div>
|
|
66
|
+
<div id="syncHint" class="small" style="margin-top:8px"></div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="card">
|
|
69
|
+
<h3>动作</h3>
|
|
70
|
+
<div class="row-btn">
|
|
71
|
+
<button onclick="runSync('github-status')">检查同步状态</button>
|
|
72
|
+
<button onclick="runSync('github-bootstrap')">首次接入自动对齐</button>
|
|
73
|
+
<button onclick="runSync('github-push')">执行 Push</button>
|
|
74
|
+
<button onclick="runSync('github-pull')">执行 Pull</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="row-btn">
|
|
77
|
+
<button onclick="toggleDaemon(true)">开启守护同步</button>
|
|
78
|
+
<button onclick="toggleDaemon(false)">关闭守护同步</button>
|
|
79
|
+
</div>
|
|
80
|
+
<pre id="syncOut" class="small"></pre>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div id="configTab" class="panel">
|
|
85
|
+
<div class="grid">
|
|
86
|
+
<div class="card wide">
|
|
87
|
+
<h3>配置</h3>
|
|
88
|
+
<form id="cfgForm">
|
|
89
|
+
<label>Config Path<input name="config_path" readonly /></label>
|
|
90
|
+
<label>Home<input name="home" /></label>
|
|
91
|
+
<label>Markdown Path<input name="markdown" /></label>
|
|
92
|
+
<label>JSONL Path<input name="jsonl" /></label>
|
|
93
|
+
<label>SQLite Path<input name="sqlite" /></label>
|
|
94
|
+
<label>Git Remote Name<input name="remote_name" /></label>
|
|
95
|
+
<label>Git Remote URL<input name="remote_url" placeholder="git@github.com:user/repo.git" /></label>
|
|
96
|
+
<label>Git Branch<input name="branch" /></label>
|
|
97
|
+
<button type="submit">保存配置</button>
|
|
98
|
+
</form>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<div id="memoryTab" class="panel">
|
|
103
|
+
<div class="grid">
|
|
104
|
+
<div class="card wide">
|
|
105
|
+
<h3>最近记忆</h3>
|
|
106
|
+
<div class="small">点击 ID 可看正文</div>
|
|
107
|
+
<table>
|
|
108
|
+
<thead><tr><th>ID</th><th>层级</th><th>类型</th><th>摘要</th><th>更新时间</th></tr></thead>
|
|
109
|
+
<tbody id="memBody"></tbody>
|
|
110
|
+
</table>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="card wide">
|
|
113
|
+
<h3>记忆正文</h3>
|
|
114
|
+
<pre id="memView" style="white-space:pre-wrap"></pre>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<script>
|
|
120
|
+
async function jget(url) { const r = await fetch(url); return await r.json(); }
|
|
121
|
+
async function jpost(url, obj) { const r = await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(obj)}); return await r.json(); }
|
|
122
|
+
|
|
123
|
+
async function loadCfg() {
|
|
124
|
+
const d = await jget('/api/config');
|
|
125
|
+
const f = document.getElementById('cfgForm');
|
|
126
|
+
for (const k of ['config_path','home','markdown','jsonl','sqlite','remote_name','remote_url','branch']) {
|
|
127
|
+
f.elements[k].value = d[k] || '';
|
|
128
|
+
}
|
|
129
|
+
const initEl = document.getElementById('initState');
|
|
130
|
+
const hintEl = document.getElementById('syncHint');
|
|
131
|
+
if (d.initialized) {
|
|
132
|
+
initEl.innerHTML = '<span class="ok">配置状态:已初始化</span>';
|
|
133
|
+
hintEl.textContent = '后台 daemon 会自动准实时同步(可手动关闭)。';
|
|
134
|
+
} else {
|
|
135
|
+
initEl.innerHTML = '<span class="warn">配置状态:未初始化(请先保存配置)</span>';
|
|
136
|
+
hintEl.textContent = '未初始化时不会启动 daemon。保存配置后会自动启用。';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function loadMem() {
|
|
141
|
+
const d = await jget('/api/memories?limit=20');
|
|
142
|
+
const b = document.getElementById('memBody');
|
|
143
|
+
b.innerHTML = '';
|
|
144
|
+
(d.items || []).forEach(x => {
|
|
145
|
+
const tr = document.createElement('tr');
|
|
146
|
+
tr.innerHTML = `<td><a href="#" data-id="${x.id}">${x.id.slice(0,10)}...</a></td><td>${x.layer}</td><td>${x.kind}</td><td>${x.summary}</td><td>${x.updated_at}</td>`;
|
|
147
|
+
tr.querySelector('a').onclick = async (e) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
const m = await jget('/api/memory?id=' + encodeURIComponent(x.id));
|
|
150
|
+
document.getElementById('memView').textContent = m.body || m.error || '';
|
|
151
|
+
};
|
|
152
|
+
b.appendChild(tr);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
document.getElementById('cfgForm').onsubmit = async (e) => {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
const f = e.target;
|
|
159
|
+
const payload = {};
|
|
160
|
+
for (const k of ['home','markdown','jsonl','sqlite','remote_name','remote_url','branch']) payload[k] = f.elements[k].value;
|
|
161
|
+
const d = await jpost('/api/config', payload);
|
|
162
|
+
document.getElementById('status').innerHTML = d.ok ? '<span class="ok">配置已保存</span>' : '<span class="err">保存失败</span>';
|
|
163
|
+
await loadCfg();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
async function runSync(mode) {
|
|
167
|
+
const d = await jpost('/api/sync', {mode});
|
|
168
|
+
document.getElementById('syncOut').textContent = JSON.stringify(d, null, 2);
|
|
169
|
+
await loadMem();
|
|
170
|
+
await loadDaemon();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function loadDaemon() {
|
|
174
|
+
const d = await jget('/api/daemon');
|
|
175
|
+
document.getElementById('daemonState').textContent = 'Daemon: ' + (d.running ? 'running' : 'stopped') + ', enabled=' + d.enabled + ', initialized=' + d.initialized;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function toggleDaemon(enabled) {
|
|
179
|
+
await jpost('/api/daemon/toggle', {enabled});
|
|
180
|
+
await loadDaemon();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function bindTabs() {
|
|
184
|
+
const btns = document.querySelectorAll('.tab-btn');
|
|
185
|
+
btns.forEach(btn => {
|
|
186
|
+
btn.onclick = () => {
|
|
187
|
+
btns.forEach(x => x.classList.remove('active'));
|
|
188
|
+
btn.classList.add('active');
|
|
189
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
190
|
+
document.getElementById(btn.dataset.tab).classList.add('active');
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
bindTabs();
|
|
196
|
+
loadCfg();
|
|
197
|
+
loadMem();
|
|
198
|
+
loadDaemon();
|
|
199
|
+
</script>
|
|
200
|
+
</body>
|
|
201
|
+
</html>
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _cfg_to_ui(cfg: dict[str, Any], cfg_path: Path) -> dict[str, Any]:
|
|
206
|
+
storage = cfg.get("storage", {})
|
|
207
|
+
gh = cfg.get("sync", {}).get("github", {})
|
|
208
|
+
return {
|
|
209
|
+
"ok": True,
|
|
210
|
+
"initialized": cfg_path.exists(),
|
|
211
|
+
"config_path": str(cfg_path),
|
|
212
|
+
"home": cfg.get("home", ""),
|
|
213
|
+
"markdown": storage.get("markdown", ""),
|
|
214
|
+
"jsonl": storage.get("jsonl", ""),
|
|
215
|
+
"sqlite": storage.get("sqlite", ""),
|
|
216
|
+
"remote_name": gh.get("remote_name", "origin"),
|
|
217
|
+
"remote_url": gh.get("remote_url", ""),
|
|
218
|
+
"branch": gh.get("branch", "main"),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def run_webui(
|
|
223
|
+
*,
|
|
224
|
+
host: str,
|
|
225
|
+
port: int,
|
|
226
|
+
cfg: dict[str, Any],
|
|
227
|
+
cfg_path: Path,
|
|
228
|
+
schema_sql_path: Path,
|
|
229
|
+
sync_runner,
|
|
230
|
+
daemon_runner=None,
|
|
231
|
+
enable_daemon: bool = True,
|
|
232
|
+
daemon_scan_interval: int = 8,
|
|
233
|
+
daemon_pull_interval: int = 30,
|
|
234
|
+
) -> None:
|
|
235
|
+
paths = resolve_paths(cfg)
|
|
236
|
+
ensure_storage(paths, schema_sql_path)
|
|
237
|
+
daemon_state: dict[str, Any] = {
|
|
238
|
+
"initialized": cfg_path.exists(),
|
|
239
|
+
"enabled": bool(enable_daemon and cfg_path.exists()),
|
|
240
|
+
"manually_disabled": False,
|
|
241
|
+
"running": False,
|
|
242
|
+
"last_result": {},
|
|
243
|
+
"scan_interval": daemon_scan_interval,
|
|
244
|
+
"pull_interval": daemon_pull_interval,
|
|
245
|
+
}
|
|
246
|
+
stop_event = threading.Event()
|
|
247
|
+
|
|
248
|
+
def daemon_loop() -> None:
|
|
249
|
+
if daemon_runner is None:
|
|
250
|
+
return
|
|
251
|
+
daemon_state["running"] = True
|
|
252
|
+
while not stop_event.is_set():
|
|
253
|
+
if not daemon_state.get("initialized", False):
|
|
254
|
+
time.sleep(1)
|
|
255
|
+
continue
|
|
256
|
+
if not daemon_state.get("enabled", True):
|
|
257
|
+
time.sleep(1)
|
|
258
|
+
continue
|
|
259
|
+
try:
|
|
260
|
+
gh = cfg.get("sync", {}).get("github", {})
|
|
261
|
+
result = daemon_runner(
|
|
262
|
+
paths=paths,
|
|
263
|
+
schema_sql_path=schema_sql_path,
|
|
264
|
+
remote_name=gh.get("remote_name", "origin"),
|
|
265
|
+
branch=gh.get("branch", "main"),
|
|
266
|
+
remote_url=gh.get("remote_url"),
|
|
267
|
+
scan_interval=daemon_scan_interval,
|
|
268
|
+
pull_interval=daemon_pull_interval,
|
|
269
|
+
once=True,
|
|
270
|
+
)
|
|
271
|
+
daemon_state["last_result"] = result
|
|
272
|
+
except Exception as exc: # pragma: no cover
|
|
273
|
+
daemon_state["last_result"] = {"ok": False, "error": str(exc)}
|
|
274
|
+
time.sleep(max(1, daemon_scan_interval))
|
|
275
|
+
daemon_state["running"] = False
|
|
276
|
+
|
|
277
|
+
daemon_thread: threading.Thread | None = None
|
|
278
|
+
if enable_daemon and daemon_runner is not None:
|
|
279
|
+
daemon_thread = threading.Thread(target=daemon_loop, name="omnimemory-daemon", daemon=True)
|
|
280
|
+
daemon_thread.start()
|
|
281
|
+
|
|
282
|
+
class Handler(BaseHTTPRequestHandler):
|
|
283
|
+
def _send_json(self, data: dict[str, Any], code: int = 200) -> None:
|
|
284
|
+
b = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
|
285
|
+
self.send_response(code)
|
|
286
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
287
|
+
self.send_header("Content-Length", str(len(b)))
|
|
288
|
+
self.end_headers()
|
|
289
|
+
self.wfile.write(b)
|
|
290
|
+
|
|
291
|
+
def _send_html(self, html: str, code: int = 200) -> None:
|
|
292
|
+
b = html.encode("utf-8")
|
|
293
|
+
self.send_response(code)
|
|
294
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
295
|
+
self.send_header("Content-Length", str(len(b)))
|
|
296
|
+
self.end_headers()
|
|
297
|
+
self.wfile.write(b)
|
|
298
|
+
|
|
299
|
+
def do_GET(self) -> None: # noqa: N802
|
|
300
|
+
parsed = urlparse(self.path)
|
|
301
|
+
if parsed.path == "/":
|
|
302
|
+
self._send_html(HTML_PAGE)
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
if parsed.path == "/api/config":
|
|
306
|
+
self._send_json(_cfg_to_ui(cfg, cfg_path))
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
if parsed.path == "/api/daemon":
|
|
310
|
+
self._send_json({"ok": True, **daemon_state})
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
if parsed.path == "/api/memories":
|
|
314
|
+
q = parse_qs(parsed.query)
|
|
315
|
+
limit = int(q.get("limit", ["20"])[0])
|
|
316
|
+
items = find_memories(paths, schema_sql_path, query="", layer=None, limit=limit)
|
|
317
|
+
self._send_json({"ok": True, "items": items})
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
if parsed.path == "/api/memory":
|
|
321
|
+
q = parse_qs(parsed.query)
|
|
322
|
+
mem_id = q.get("id", [""])[0]
|
|
323
|
+
if not mem_id:
|
|
324
|
+
self._send_json({"ok": False, "error": "missing id"}, 400)
|
|
325
|
+
return
|
|
326
|
+
try:
|
|
327
|
+
with sqlite3.connect(paths.sqlite_path) as conn:
|
|
328
|
+
row = conn.execute(
|
|
329
|
+
"SELECT body_md_path FROM memories WHERE id = ?",
|
|
330
|
+
(mem_id,),
|
|
331
|
+
).fetchone()
|
|
332
|
+
if not row:
|
|
333
|
+
self._send_json({"ok": False, "error": "not found"}, 404)
|
|
334
|
+
return
|
|
335
|
+
md_path = paths.markdown_root / row[0]
|
|
336
|
+
self._send_json({"ok": True, "body": md_path.read_text(encoding="utf-8")})
|
|
337
|
+
except Exception as exc: # pragma: no cover
|
|
338
|
+
self._send_json({"ok": False, "error": str(exc)}, 500)
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
self._send_json({"ok": False, "error": "not found"}, 404)
|
|
342
|
+
|
|
343
|
+
def do_POST(self) -> None: # noqa: N802
|
|
344
|
+
parsed = urlparse(self.path)
|
|
345
|
+
length = int(self.headers.get("Content-Length", "0") or "0")
|
|
346
|
+
raw = self.rfile.read(length) if length else b"{}"
|
|
347
|
+
data = json.loads(raw.decode("utf-8") or "{}")
|
|
348
|
+
|
|
349
|
+
if parsed.path == "/api/config":
|
|
350
|
+
cfg["home"] = data.get("home", cfg.get("home", ""))
|
|
351
|
+
cfg.setdefault("storage", {})
|
|
352
|
+
cfg["storage"]["markdown"] = data.get("markdown", cfg["storage"].get("markdown", ""))
|
|
353
|
+
cfg["storage"]["jsonl"] = data.get("jsonl", cfg["storage"].get("jsonl", ""))
|
|
354
|
+
cfg["storage"]["sqlite"] = data.get("sqlite", cfg["storage"].get("sqlite", ""))
|
|
355
|
+
cfg.setdefault("sync", {}).setdefault("github", {})
|
|
356
|
+
cfg["sync"]["github"]["remote_name"] = data.get("remote_name", "origin")
|
|
357
|
+
cfg["sync"]["github"]["remote_url"] = data.get("remote_url", "")
|
|
358
|
+
cfg["sync"]["github"]["branch"] = data.get("branch", "main")
|
|
359
|
+
try:
|
|
360
|
+
save_config(cfg_path, cfg)
|
|
361
|
+
# Refresh resolved path pointers after config update.
|
|
362
|
+
nonlocal paths
|
|
363
|
+
paths = resolve_paths(cfg)
|
|
364
|
+
ensure_storage(paths, schema_sql_path)
|
|
365
|
+
was_initialized = daemon_state.get("initialized", False)
|
|
366
|
+
daemon_state["initialized"] = True
|
|
367
|
+
if not was_initialized and enable_daemon:
|
|
368
|
+
daemon_state["enabled"] = not daemon_state.get("manually_disabled", False)
|
|
369
|
+
self._send_json({"ok": True})
|
|
370
|
+
except Exception as exc: # pragma: no cover
|
|
371
|
+
self._send_json({"ok": False, "error": str(exc)}, 500)
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
if parsed.path == "/api/sync":
|
|
375
|
+
if not daemon_state.get("initialized", False):
|
|
376
|
+
self._send_json({"ok": False, "error": "config not initialized; save config first"}, 400)
|
|
377
|
+
return
|
|
378
|
+
mode = data.get("mode", "github-status")
|
|
379
|
+
gh = cfg.get("sync", {}).get("github", {})
|
|
380
|
+
try:
|
|
381
|
+
out = sync_runner(
|
|
382
|
+
paths,
|
|
383
|
+
schema_sql_path,
|
|
384
|
+
mode,
|
|
385
|
+
remote_name=gh.get("remote_name", "origin"),
|
|
386
|
+
branch=gh.get("branch", "main"),
|
|
387
|
+
remote_url=gh.get("remote_url"),
|
|
388
|
+
commit_message="chore(memory): sync from webui",
|
|
389
|
+
)
|
|
390
|
+
self._send_json(out)
|
|
391
|
+
except Exception as exc: # pragma: no cover
|
|
392
|
+
self._send_json({"ok": False, "error": str(exc)}, 500)
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
if parsed.path == "/api/daemon/toggle":
|
|
396
|
+
desired = bool(data.get("enabled", True))
|
|
397
|
+
daemon_state["manually_disabled"] = not desired
|
|
398
|
+
daemon_state["enabled"] = bool(desired and daemon_state.get("initialized", False))
|
|
399
|
+
self._send_json(
|
|
400
|
+
{
|
|
401
|
+
"ok": True,
|
|
402
|
+
"enabled": daemon_state["enabled"],
|
|
403
|
+
"initialized": daemon_state["initialized"],
|
|
404
|
+
"running": daemon_state["running"],
|
|
405
|
+
"last_result": daemon_state.get("last_result", {}),
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
self._send_json({"ok": False, "error": "not found"}, 404)
|
|
411
|
+
|
|
412
|
+
server = ThreadingHTTPServer((host, port), Handler)
|
|
413
|
+
print(f"WebUI running on http://{host}:{port} (daemon={'on' if enable_daemon else 'off'})")
|
|
414
|
+
try:
|
|
415
|
+
server.serve_forever()
|
|
416
|
+
finally:
|
|
417
|
+
stop_event.set()
|
|
418
|
+
if daemon_thread is not None:
|
|
419
|
+
daemon_thread.join(timeout=1.5)
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "omnimemory-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "OmniMemory CLI and bootstrap runner",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"omnimemory": "bin/omnimemory"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"omnimemory",
|
|
13
|
+
"scripts",
|
|
14
|
+
"templates",
|
|
15
|
+
"db",
|
|
16
|
+
"spec",
|
|
17
|
+
"README.md",
|
|
18
|
+
"docs/quickstart-10min.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepack": "find omnimemory -name '__pycache__' -type d -prune -exec rm -rf {} +",
|
|
22
|
+
"start": "./bin/omnimemory start --host 127.0.0.1 --port 8765",
|
|
23
|
+
"verify": "bash scripts/verify_phase_d.sh",
|
|
24
|
+
"pack:check": "npm pack --dry-run"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/YOUR_ORG/omnimemory.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/YOUR_ORG/omnimemory#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/YOUR_ORG/omnimemory/issues"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ $# -lt 2 ]]; then
|
|
5
|
+
echo "Usage: bash scripts/attach_project.sh <project_path> <project_id>"
|
|
6
|
+
exit 1
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
10
|
+
PROJECT_PATH="$1"
|
|
11
|
+
PROJECT_ID="$2"
|
|
12
|
+
|
|
13
|
+
if [[ ! -d "$PROJECT_PATH" ]]; then
|
|
14
|
+
echo "Project path not found: $PROJECT_PATH"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
cp -f "$ROOT_DIR/templates/project-minimal/.omni-memory.json" "$PROJECT_PATH/.omni-memory.json"
|
|
19
|
+
cp -f "$ROOT_DIR/templates/project-minimal/.omni-memory-session.md" "$PROJECT_PATH/.omni-memory-session.md"
|
|
20
|
+
cp -f "$ROOT_DIR/templates/project-minimal/.omni-memory-ignore" "$PROJECT_PATH/.omni-memory-ignore"
|
|
21
|
+
|
|
22
|
+
sed -i.bak "s/replace-with-project-id/$PROJECT_ID/g" "$PROJECT_PATH/.omni-memory.json"
|
|
23
|
+
rm -f "$PROJECT_PATH/.omni-memory.json.bak"
|
|
24
|
+
|
|
25
|
+
echo "Attached OmniMemory to: $PROJECT_PATH (project_id=$PROJECT_ID)"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# One-command bootstrap for new device / new account.
|
|
5
|
+
# Example:
|
|
6
|
+
# bash scripts/bootstrap.sh --repo git@github.com:YOUR_USER/omnimemory.git
|
|
7
|
+
# bash scripts/bootstrap.sh --repo https://github.com/YOUR_USER/omnimemory.git --attach ~/code/myproj --project-id myproj
|
|
8
|
+
|
|
9
|
+
TARGET_HOME="${OMNIMEMORY_HOME:-$HOME/.omnimemory}"
|
|
10
|
+
APP_DIR="$TARGET_HOME/app"
|
|
11
|
+
REPO_URL=""
|
|
12
|
+
BRANCH="main"
|
|
13
|
+
REMOTE_NAME="origin"
|
|
14
|
+
REMOTE_URL=""
|
|
15
|
+
ATTACH_PATH=""
|
|
16
|
+
PROJECT_ID=""
|
|
17
|
+
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--repo)
|
|
21
|
+
REPO_URL="$2"
|
|
22
|
+
shift 2
|
|
23
|
+
;;
|
|
24
|
+
--branch)
|
|
25
|
+
BRANCH="$2"
|
|
26
|
+
shift 2
|
|
27
|
+
;;
|
|
28
|
+
--remote-name)
|
|
29
|
+
REMOTE_NAME="$2"
|
|
30
|
+
shift 2
|
|
31
|
+
;;
|
|
32
|
+
--remote-url)
|
|
33
|
+
REMOTE_URL="$2"
|
|
34
|
+
shift 2
|
|
35
|
+
;;
|
|
36
|
+
--attach)
|
|
37
|
+
ATTACH_PATH="$2"
|
|
38
|
+
shift 2
|
|
39
|
+
;;
|
|
40
|
+
--project-id)
|
|
41
|
+
PROJECT_ID="$2"
|
|
42
|
+
shift 2
|
|
43
|
+
;;
|
|
44
|
+
*)
|
|
45
|
+
echo "Unknown arg: $1"
|
|
46
|
+
exit 1
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
if [[ -z "$REPO_URL" ]]; then
|
|
52
|
+
echo "Usage: bash scripts/bootstrap.sh --repo <git_repo_url> [--branch main] [--remote-url <memory_git_url>] [--attach <project_path> --project-id <id>]"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
mkdir -p "$TARGET_HOME"
|
|
57
|
+
|
|
58
|
+
if [[ -d "$APP_DIR/.git" ]]; then
|
|
59
|
+
git -C "$APP_DIR" fetch --all --prune
|
|
60
|
+
git -C "$APP_DIR" checkout "$BRANCH"
|
|
61
|
+
git -C "$APP_DIR" pull --rebase
|
|
62
|
+
else
|
|
63
|
+
git clone --branch "$BRANCH" "$REPO_URL" "$APP_DIR"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
cd "$APP_DIR"
|
|
67
|
+
bash scripts/install.sh --remote-name "$REMOTE_NAME" --branch "$BRANCH" ${REMOTE_URL:+--remote-url "$REMOTE_URL"}
|
|
68
|
+
|
|
69
|
+
if [[ -n "$ATTACH_PATH" ]]; then
|
|
70
|
+
if [[ -z "$PROJECT_ID" ]]; then
|
|
71
|
+
PROJECT_ID="$(basename "$ATTACH_PATH")"
|
|
72
|
+
fi
|
|
73
|
+
bash scripts/attach_project.sh "$ATTACH_PATH" "$PROJECT_ID"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
echo "Bootstrap done."
|
|
77
|
+
echo "Run: $TARGET_HOME/bin/omnimemory start --host 127.0.0.1 --port 8765"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
if [[ $# -lt 1 ]]; then
|
|
5
|
+
echo "Usage: bash scripts/detach_project.sh <project_path>"
|
|
6
|
+
exit 1
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
PROJECT_PATH="$1"
|
|
10
|
+
|
|
11
|
+
if [[ ! -d "$PROJECT_PATH" ]]; then
|
|
12
|
+
echo "Project path not found: $PROJECT_PATH"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
rm -f "$PROJECT_PATH/.omni-memory.json" \
|
|
17
|
+
"$PROJECT_PATH/.omni-memory-session.md" \
|
|
18
|
+
"$PROJECT_PATH/.omni-memory-ignore" \
|
|
19
|
+
"$PROJECT_PATH/.omni-memory-hooks.sh"
|
|
20
|
+
|
|
21
|
+
echo "Detached OmniMemory from: $PROJECT_PATH"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
TARGET_HOME="${OMNIMEMORY_HOME:-$HOME/.omnimemory}"
|
|
6
|
+
|
|
7
|
+
WIZARD=0
|
|
8
|
+
REMOTE_NAME="origin"
|
|
9
|
+
BRANCH="main"
|
|
10
|
+
REMOTE_URL=""
|
|
11
|
+
|
|
12
|
+
while [[ $# -gt 0 ]]; do
|
|
13
|
+
case "$1" in
|
|
14
|
+
--wizard)
|
|
15
|
+
WIZARD=1
|
|
16
|
+
shift
|
|
17
|
+
;;
|
|
18
|
+
--remote-name)
|
|
19
|
+
REMOTE_NAME="$2"
|
|
20
|
+
shift 2
|
|
21
|
+
;;
|
|
22
|
+
--branch)
|
|
23
|
+
BRANCH="$2"
|
|
24
|
+
shift 2
|
|
25
|
+
;;
|
|
26
|
+
--remote-url)
|
|
27
|
+
REMOTE_URL="$2"
|
|
28
|
+
shift 2
|
|
29
|
+
;;
|
|
30
|
+
*)
|
|
31
|
+
echo "Unknown arg: $1"
|
|
32
|
+
exit 1
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
if [[ "$WIZARD" -eq 1 ]]; then
|
|
38
|
+
echo "[OmniMemory Install Wizard]"
|
|
39
|
+
read -r -p "Install home [$TARGET_HOME]: " input_home
|
|
40
|
+
TARGET_HOME="${input_home:-$TARGET_HOME}"
|
|
41
|
+
|
|
42
|
+
read -r -p "Git remote name [$REMOTE_NAME]: " input_remote_name
|
|
43
|
+
REMOTE_NAME="${input_remote_name:-$REMOTE_NAME}"
|
|
44
|
+
|
|
45
|
+
read -r -p "Git branch [$BRANCH]: " input_branch
|
|
46
|
+
BRANCH="${input_branch:-$BRANCH}"
|
|
47
|
+
|
|
48
|
+
read -r -p "Git remote url (optional, e.g. git@github.com:user/repo.git): " input_remote_url
|
|
49
|
+
REMOTE_URL="${input_remote_url:-$REMOTE_URL}"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
mkdir -p "$TARGET_HOME"
|
|
53
|
+
mkdir -p "$TARGET_HOME/data/markdown"/{instant,short,long,archive}
|
|
54
|
+
mkdir -p "$TARGET_HOME/data/jsonl"
|
|
55
|
+
mkdir -p "$TARGET_HOME/spec" "$TARGET_HOME/db" "$TARGET_HOME/docs"
|
|
56
|
+
mkdir -p "$TARGET_HOME/bin" "$TARGET_HOME/lib"
|
|
57
|
+
|
|
58
|
+
cp -f "$ROOT_DIR/spec/"*.json "$TARGET_HOME/spec/"
|
|
59
|
+
cp -f "$ROOT_DIR/db/schema.sql" "$TARGET_HOME/db/schema.sql"
|
|
60
|
+
cp -f "$ROOT_DIR/docs/architecture.md" "$TARGET_HOME/docs/architecture.md"
|
|
61
|
+
rm -rf "$TARGET_HOME/lib/omnimemory"
|
|
62
|
+
cp -R "$ROOT_DIR/omnimemory" "$TARGET_HOME/lib/omnimemory"
|
|
63
|
+
|
|
64
|
+
cat > "$TARGET_HOME/bin/omnimemory" <<SH
|
|
65
|
+
#!/usr/bin/env bash
|
|
66
|
+
set -euo pipefail
|
|
67
|
+
export OMNIMEMORY_HOME="$TARGET_HOME"
|
|
68
|
+
PYTHONPATH="$TARGET_HOME/lib\${PYTHONPATH:+:\$PYTHONPATH}" exec python3 -m omnimemory.cli "\$@"
|
|
69
|
+
SH
|
|
70
|
+
chmod +x "$TARGET_HOME/bin/omnimemory"
|
|
71
|
+
|
|
72
|
+
cat > "$TARGET_HOME/omnimemory.config.json" <<JSON
|
|
73
|
+
{
|
|
74
|
+
"version": "0.1.0",
|
|
75
|
+
"home": "$TARGET_HOME",
|
|
76
|
+
"storage": {
|
|
77
|
+
"markdown": "$TARGET_HOME/data/markdown",
|
|
78
|
+
"jsonl": "$TARGET_HOME/data/jsonl",
|
|
79
|
+
"sqlite": "$TARGET_HOME/data/omnimemory.db"
|
|
80
|
+
},
|
|
81
|
+
"sync": {
|
|
82
|
+
"github": {
|
|
83
|
+
"remote_name": "$REMOTE_NAME",
|
|
84
|
+
"remote_url": "$REMOTE_URL",
|
|
85
|
+
"branch": "$BRANCH"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
JSON
|
|
90
|
+
|
|
91
|
+
echo "Installed OmniMemory skeleton at: $TARGET_HOME"
|
|
92
|
+
echo "CLI path: $TARGET_HOME/bin/omnimemory"
|
|
93
|
+
echo "Config path: $TARGET_HOME/omnimemory.config.json"
|
|
94
|
+
echo "Start App: $TARGET_HOME/bin/omnimemory start --host 127.0.0.1 --port 8765"
|