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.
@@ -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"