omnimem 0.1.5 → 0.1.7

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.
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.1.4"
2
+ __version__ = "0.1.7"
package/omnimem/webui.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import os
4
5
  import sqlite3
5
6
  import threading
6
7
  import time
@@ -74,6 +75,7 @@ HTML_PAGE = """<!doctype html>
74
75
  <div class=\"tabs\">
75
76
  <button class=\"tab-btn active\" data-tab=\"statusTab\" data-i18n=\"tab_status\">Status & Actions</button>
76
77
  <button class=\"tab-btn\" data-tab=\"configTab\" data-i18n=\"tab_config\">Configuration</button>
78
+ <button class=\"tab-btn\" data-tab=\"projectTab\" data-i18n=\"tab_project\">Project Integration</button>
77
79
  <button class=\"tab-btn\" data-tab=\"memoryTab\" data-i18n=\"tab_memory\">Memory</button>
78
80
  </div>
79
81
  </div>
@@ -121,6 +123,36 @@ HTML_PAGE = """<!doctype html>
121
123
  </div>
122
124
  </div>
123
125
 
126
+ <div id=\"projectTab\" class=\"panel\">
127
+ <div class=\"grid\">
128
+ <div class=\"card wide\">
129
+ <h3 data-i18n=\"project_title\">Project Integration</h3>
130
+ <label><span data-i18n=\"project_path\">Project Path</span><input id=\"projectPath\" placeholder=\"/path/to/your/project\" /></label>
131
+ <div class=\"row-btn\">
132
+ <button id=\"btnBrowseProject\" data-i18n=\"btn_browse_project\">Browse Directory</button>
133
+ <button id=\"btnUseCwd\" data-i18n=\"btn_use_cwd\">Use Server CWD</button>
134
+ </div>
135
+ <div id=\"browserPanel\" class=\"card\" style=\"margin-top:10px; display:none;\">
136
+ <div class=\"small\" data-i18n=\"browser_title\">Directory Browser</div>
137
+ <div id=\"browserPath\" class=\"small\" style=\"margin:8px 0\"></div>
138
+ <div class=\"row-btn\">
139
+ <button id=\"btnBrowserUp\" data-i18n=\"btn_browser_up\">Up</button>
140
+ <button id=\"btnBrowserSelect\" data-i18n=\"btn_browser_select\">Select This Directory</button>
141
+ <button id=\"btnBrowserClose\" data-i18n=\"btn_browser_close\">Close</button>
142
+ </div>
143
+ <div id=\"browserList\" class=\"small\" style=\"margin-top:8px\"></div>
144
+ </div>
145
+ <label><span data-i18n=\"project_id\">Project ID</span><input id=\"projectId\" placeholder=\"my-project\" /></label>
146
+ <div class=\"row-btn\">
147
+ <button id=\"btnProjectAttach\" data-i18n=\"btn_project_attach\">Attach Project + Install Agent Rules</button>
148
+ <button id=\"btnProjectDetach\" data-i18n=\"btn_project_detach\">Detach Project</button>
149
+ </div>
150
+ <div class=\"small\" data-i18n=\"project_hint\">Attach will create .omnimem files and inject managed memory protocol blocks into AGENTS.md / CLAUDE.md / .cursorrules.</div>
151
+ <pre id=\"projectOut\" class=\"small\"></pre>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
124
156
  <div id=\"memoryTab\" class=\"panel\">
125
157
  <div class=\"grid\">
126
158
  <div class=\"card wide\">
@@ -151,14 +183,20 @@ HTML_PAGE = """<!doctype html>
151
183
  const I18N = {
152
184
  en: {
153
185
  title: 'OmniMem WebUI', subtitle: 'Simple mode: Status & Actions / Configuration / Memory', language: 'Language',
154
- tab_status: 'Status & Actions', tab_config: 'Configuration', tab_memory: 'Memory',
186
+ tab_status: 'Status & Actions', tab_config: 'Configuration', tab_project: 'Project Integration', tab_memory: 'Memory',
155
187
  system_status: 'System Status', actions: 'Actions',
156
188
  btn_status: 'Check Sync Status', btn_bootstrap: 'Bootstrap Device Sync', btn_push: 'Push', btn_pull: 'Pull',
157
189
  btn_daemon_on: 'Enable Daemon', btn_daemon_off: 'Disable Daemon',
158
190
  config_title: 'Configuration', cfg_path: 'Config Path', cfg_home: 'Home', cfg_markdown: 'Markdown Path', cfg_jsonl: 'JSONL Path', cfg_sqlite: 'SQLite Path', cfg_remote_name: 'Git Remote Name', cfg_remote_url: 'Git Remote URL', cfg_branch: 'Git Branch', btn_save: 'Save Configuration',
159
191
  mem_recent: 'Recent Memories', mem_hint: 'Click an ID to open full content', mem_content: 'Memory Content',
160
192
  th_id: 'ID', th_layer: 'Layer', th_kind: 'Kind', th_summary: 'Summary', th_updated: 'Updated At',
193
+ project_title: 'Project Integration', project_path: 'Project Path', project_id: 'Project ID',
194
+ btn_browse_project: 'Browse Directory', btn_use_cwd: 'Use Server CWD',
195
+ browser_title: 'Directory Browser', btn_browser_up: 'Up', btn_browser_select: 'Select This Directory', btn_browser_close: 'Close',
196
+ btn_project_attach: 'Attach Project + Install Agent Rules', btn_project_detach: 'Detach Project',
197
+ project_hint: 'Attach will create .omnimem files and inject managed memory protocol blocks into AGENTS.md / CLAUDE.md / .cursorrules.',
161
198
  cfg_saved: 'Configuration saved', cfg_failed: 'Save failed',
199
+ project_attach_ok: 'Project attached', project_detach_ok: 'Project detached', project_failed: 'Project action failed',
162
200
  init_ok: 'Config state: initialized', init_hint_ok: 'Daemon runs quasi-realtime sync in background (can be disabled).',
163
201
  init_missing: 'Config state: not initialized (save configuration first)', init_hint_missing: 'Daemon is disabled until configuration is initialized.',
164
202
  daemon_state: (d) => `Daemon: ${d.running ? 'running' : 'stopped'}, enabled=${d.enabled}, initialized=${d.initialized}`
@@ -272,6 +310,7 @@ HTML_PAGE = """<!doctype html>
272
310
  let currentLang = safeGetLang();
273
311
  if (!I18N[currentLang]) currentLang = 'en';
274
312
  let daemonCache = { running:false, enabled:false, initialized:false };
313
+ let browserPath = '';
275
314
 
276
315
  function t(key) {
277
316
  const dict = I18N[currentLang] || I18N.en;
@@ -363,6 +402,47 @@ HTML_PAGE = """<!doctype html>
363
402
  await loadDaemon();
364
403
  }
365
404
 
405
+ async function attachProject() {
406
+ const project_path = document.getElementById('projectPath').value.trim();
407
+ const project_id = document.getElementById('projectId').value.trim();
408
+ const out = document.getElementById('projectOut');
409
+ const d = await jpost('/api/project/attach', {project_path, project_id});
410
+ out.textContent = JSON.stringify(d, null, 2);
411
+ document.getElementById('status').innerHTML = d.ok ? `<span class=\"ok\">${t('project_attach_ok')}</span>` : `<span class=\"err\">${t('project_failed')}</span>`;
412
+ }
413
+
414
+ async function detachProject() {
415
+ const project_path = document.getElementById('projectPath').value.trim();
416
+ const out = document.getElementById('projectOut');
417
+ const d = await jpost('/api/project/detach', {project_path});
418
+ out.textContent = JSON.stringify(d, null, 2);
419
+ document.getElementById('status').innerHTML = d.ok ? `<span class=\"ok\">${t('project_detach_ok')}</span>` : `<span class=\"err\">${t('project_failed')}</span>`;
420
+ }
421
+
422
+ function escHtml(v) {
423
+ return String(v).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('\"', '&quot;');
424
+ }
425
+
426
+ async function listDirs(path) {
427
+ const d = await jget('/api/fs/list?path=' + encodeURIComponent(path || ''));
428
+ if (!d.ok) {
429
+ document.getElementById('browserList').innerHTML = `<span class=\"err\">${escHtml(d.error || 'list failed')}</span>`;
430
+ return;
431
+ }
432
+ browserPath = d.path || '';
433
+ document.getElementById('browserPath').textContent = browserPath;
434
+ const rows = (d.items || [])
435
+ .map(x => `<div><a href=\"#\" data-path=\"${escHtml(x.path)}\">${escHtml(x.name)}/</a></div>`)
436
+ .join('');
437
+ document.getElementById('browserList').innerHTML = rows || '<span class=\"small\">(empty)</span>';
438
+ document.querySelectorAll('#browserList a').forEach(a => {
439
+ a.onclick = (e) => {
440
+ e.preventDefault();
441
+ listDirs(a.dataset.path || '');
442
+ };
443
+ });
444
+ }
445
+
366
446
  function bindTabs() {
367
447
  const btns = document.querySelectorAll('.tab-btn');
368
448
  btns.forEach(btn => {
@@ -389,6 +469,41 @@ HTML_PAGE = """<!doctype html>
389
469
  document.getElementById('btnSyncPull').onclick = () => runSync('github-pull');
390
470
  document.getElementById('btnDaemonOn').onclick = () => toggleDaemon(true);
391
471
  document.getElementById('btnDaemonOff').onclick = () => toggleDaemon(false);
472
+ document.getElementById('btnProjectAttach').onclick = () => attachProject();
473
+ document.getElementById('btnProjectDetach').onclick = () => detachProject();
474
+ document.getElementById('btnBrowseProject').onclick = async () => {
475
+ document.getElementById('browserPanel').style.display = 'block';
476
+ await listDirs(document.getElementById('projectPath').value.trim() || '');
477
+ };
478
+ document.getElementById('btnBrowserUp').onclick = async () => {
479
+ if (!browserPath) return;
480
+ const p = browserPath.replace(/\/+$/, '');
481
+ const i = p.lastIndexOf('/');
482
+ const up = i > 0 ? p.slice(0, i) : '/';
483
+ await listDirs(up);
484
+ };
485
+ document.getElementById('btnBrowserSelect').onclick = () => {
486
+ document.getElementById('projectPath').value = browserPath;
487
+ const pid = document.getElementById('projectId');
488
+ if (!pid.value.trim() && browserPath) {
489
+ const s = browserPath.replace(/\/+$/, '').split('/');
490
+ pid.value = s[s.length - 1] || 'project';
491
+ }
492
+ };
493
+ document.getElementById('btnBrowserClose').onclick = () => {
494
+ document.getElementById('browserPanel').style.display = 'none';
495
+ };
496
+ document.getElementById('btnUseCwd').onclick = async () => {
497
+ const d = await jget('/api/fs/cwd');
498
+ if (d.ok) {
499
+ document.getElementById('projectPath').value = d.cwd;
500
+ const pid = document.getElementById('projectId');
501
+ if (!pid.value.trim()) {
502
+ const s = d.cwd.replace(/\/+$/, '').split('/');
503
+ pid.value = s[s.length - 1] || 'project';
504
+ }
505
+ }
506
+ };
392
507
  }
393
508
 
394
509
  window.addEventListener('error', (e) => {
@@ -425,6 +540,137 @@ def _cfg_to_ui(cfg: dict[str, Any], cfg_path: Path) -> dict[str, Any]:
425
540
  }
426
541
 
427
542
 
543
+ def _upsert_managed_block(path: Path, block: str) -> None:
544
+ start = "<!-- OMNIMEM:START -->"
545
+ end = "<!-- OMNIMEM:END -->"
546
+ managed = f"{start}\n{block.rstrip()}\n{end}\n"
547
+ if path.exists():
548
+ old = path.read_text(encoding="utf-8")
549
+ if start in old and end in old:
550
+ left = old.split(start, 1)[0].rstrip()
551
+ right = old.split(end, 1)[1].lstrip()
552
+ new_text = f"{left}\n\n{managed}"
553
+ if right:
554
+ new_text += f"\n{right}"
555
+ path.write_text(new_text, encoding="utf-8")
556
+ return
557
+ sep = "\n\n" if old and not old.endswith("\n\n") else ""
558
+ path.write_text(old + sep + managed, encoding="utf-8")
559
+ return
560
+ path.write_text(managed, encoding="utf-8")
561
+
562
+
563
+ def _agent_protocol_block(project_id: str) -> str:
564
+ return (
565
+ "# OmniMem Memory Protocol\n"
566
+ "\n"
567
+ f"- Project ID: `{project_id}`\n"
568
+ "- Session start: run `omnimem brief --project-id <PROJECT_ID> --limit 8` and use it as active context.\n"
569
+ "- During task: when a stable decision/fact appears, run `omnimem write` with concise summary + evidence.\n"
570
+ "- Phase end: run `omnimem checkpoint` with goal/result/next-step/risks.\n"
571
+ "- If confidence is low or info is temporary, store in `instant`/`short`; promote to `long` only when repeated and stable.\n"
572
+ "- Never write raw secrets. Use credential references only (for example `op://...` or `env://...`).\n"
573
+ )
574
+
575
+
576
+ def _attach_project_in_webui(project_path: str, project_id: str, cfg_home: str) -> dict[str, Any]:
577
+ if not project_path:
578
+ return {"ok": False, "error": "project_path is required"}
579
+ project = Path(project_path).expanduser().resolve()
580
+ if not project.exists() or not project.is_dir():
581
+ return {"ok": False, "error": f"project path not found: {project}"}
582
+ if not project_id:
583
+ project_id = project.name
584
+
585
+ repo_root = Path(__file__).resolve().parent.parent
586
+ tpl = repo_root / "templates" / "project-minimal"
587
+ created: list[str] = []
588
+ updated: list[str] = []
589
+
590
+ files = [
591
+ (tpl / ".omnimem.json", project / ".omnimem.json"),
592
+ (tpl / ".omnimem-session.md", project / ".omnimem-session.md"),
593
+ (tpl / ".omnimem-ignore", project / ".omnimem-ignore"),
594
+ ]
595
+ for src, dst in files:
596
+ text = src.read_text(encoding="utf-8")
597
+ text = text.replace("replace-with-project-id", project_id)
598
+ text = text.replace("~/.omnimem", cfg_home or "~/.omnimem")
599
+ exists = dst.exists()
600
+ dst.write_text(text, encoding="utf-8")
601
+ (updated if exists else created).append(str(dst))
602
+
603
+ block = _agent_protocol_block(project_id=project_id)
604
+ managed_targets = [
605
+ project / "AGENTS.md",
606
+ project / "CLAUDE.md",
607
+ project / ".cursorrules",
608
+ ]
609
+ for fp in managed_targets:
610
+ exists = fp.exists()
611
+ _upsert_managed_block(fp, block)
612
+ (updated if exists else created).append(str(fp))
613
+
614
+ cursor_rule = project / ".cursor" / "rules" / "omnimem.mdc"
615
+ cursor_exists = cursor_rule.exists()
616
+ cursor_rule.parent.mkdir(parents=True, exist_ok=True)
617
+ cursor_rule.write_text(
618
+ (
619
+ "---\n"
620
+ "description: OmniMem project memory protocol\n"
621
+ "alwaysApply: true\n"
622
+ "---\n\n"
623
+ + block
624
+ ),
625
+ encoding="utf-8",
626
+ )
627
+ (updated if cursor_exists else created).append(str(cursor_rule))
628
+
629
+ return {
630
+ "ok": True,
631
+ "project_path": str(project),
632
+ "project_id": project_id,
633
+ "created": created,
634
+ "updated": updated,
635
+ }
636
+
637
+
638
+ def _detach_project_in_webui(project_path: str) -> dict[str, Any]:
639
+ if not project_path:
640
+ return {"ok": False, "error": "project_path is required"}
641
+ project = Path(project_path).expanduser().resolve()
642
+ if not project.exists() or not project.is_dir():
643
+ return {"ok": False, "error": f"project path not found: {project}"}
644
+
645
+ removed: list[str] = []
646
+ for name in [
647
+ ".omnimem.json",
648
+ ".omnimem-session.md",
649
+ ".omnimem-ignore",
650
+ ".cursorrules",
651
+ "CLAUDE.md",
652
+ "AGENTS.md",
653
+ ".cursor/rules/omnimem.mdc",
654
+ ]:
655
+ fp = project / name
656
+ if fp.exists():
657
+ txt = fp.read_text(encoding="utf-8", errors="ignore")
658
+ if "<!-- OMNIMEM:START -->" in txt and "<!-- OMNIMEM:END -->" in txt:
659
+ start = txt.index("<!-- OMNIMEM:START -->")
660
+ end = txt.index("<!-- OMNIMEM:END -->") + len("<!-- OMNIMEM:END -->")
661
+ new_txt = (txt[:start] + txt[end:]).strip()
662
+ if new_txt:
663
+ fp.write_text(new_txt + "\n", encoding="utf-8")
664
+ else:
665
+ fp.unlink()
666
+ removed.append(str(fp))
667
+ continue
668
+ if fp.name in {".omnimem.json", ".omnimem-session.md", ".omnimem-ignore", "omnimem.mdc"}:
669
+ fp.unlink()
670
+ removed.append(str(fp))
671
+ return {"ok": True, "project_path": str(project), "removed": removed}
672
+
673
+
428
674
  def run_webui(
429
675
  *,
430
676
  host: str,
@@ -516,6 +762,40 @@ def run_webui(
516
762
  self._send_json({"ok": True, **daemon_state})
517
763
  return
518
764
 
765
+ if parsed.path == "/api/fs/cwd":
766
+ self._send_json({"ok": True, "cwd": str(Path.cwd())})
767
+ return
768
+
769
+ if parsed.path == "/api/fs/list":
770
+ q = parse_qs(parsed.query)
771
+ raw_path = q.get("path", [""])[0].strip()
772
+ base = Path(raw_path).expanduser() if raw_path else Path.home()
773
+ try:
774
+ p = base.resolve()
775
+ if not p.exists() or not p.is_dir():
776
+ self._send_json({"ok": False, "error": f"not a directory: {p}"}, 400)
777
+ return
778
+ items = []
779
+ for child in sorted(p.iterdir(), key=lambda x: x.name.lower()):
780
+ if child.is_dir() and not child.name.startswith("."):
781
+ items.append({"name": child.name, "path": str(child)})
782
+ if len(items) >= 200:
783
+ break
784
+ self._send_json({"ok": True, "path": str(p), "items": items})
785
+ except Exception as exc: # pragma: no cover
786
+ self._send_json({"ok": False, "error": str(exc)}, 500)
787
+ return
788
+
789
+ if parsed.path == "/api/project/defaults":
790
+ self._send_json(
791
+ {
792
+ "ok": True,
793
+ "project_path": "",
794
+ "project_id": "",
795
+ }
796
+ )
797
+ return
798
+
519
799
  if parsed.path == "/api/memories":
520
800
  q = parse_qs(parsed.query)
521
801
  limit = int(q.get("limit", ["20"])[0])
@@ -612,6 +892,26 @@ def run_webui(
612
892
  )
613
893
  return
614
894
 
895
+ if parsed.path == "/api/project/attach":
896
+ try:
897
+ out = _attach_project_in_webui(
898
+ project_path=str(data.get("project_path", "")).strip(),
899
+ project_id=str(data.get("project_id", "")).strip(),
900
+ cfg_home=str(cfg.get("home", "")).strip(),
901
+ )
902
+ self._send_json(out, 200 if out.get("ok") else 400)
903
+ except Exception as exc: # pragma: no cover
904
+ self._send_json({"ok": False, "error": str(exc)}, 500)
905
+ return
906
+
907
+ if parsed.path == "/api/project/detach":
908
+ try:
909
+ out = _detach_project_in_webui(str(data.get("project_path", "")).strip())
910
+ self._send_json(out, 200 if out.get("ok") else 400)
911
+ except Exception as exc: # pragma: no cover
912
+ self._send_json({"ok": False, "error": str(exc)}, 500)
913
+ return
914
+
615
915
  self._send_json({"ok": False, "error": "not found"}, 404)
616
916
 
617
917
  server = ThreadingHTTPServer((host, port), Handler)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnimem",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "description": "OmniMem CLI and bootstrap runner",
6
6
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- # OmniMemory Session Entry
1
+ # OmniMem Session Entry
2
2
 
3
3
  - Current phase:
4
4
  - Current objective:
@@ -8,6 +8,6 @@
8
8
 
9
9
  ## Quick start
10
10
 
11
- 1. Run `omnimemory brief --project <project_id>`
11
+ 1. Run `omnimem brief --project-id <project_id>`
12
12
  2. Review top 3 recent checkpoints
13
13
  3. Continue task or start a new session
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "version": "0.1.0",
3
3
  "project_id": "replace-with-project-id",
4
- "memory_home": "~/.omnimemory",
4
+ "memory_home": "~/.omnimem",
5
5
  "default_tags": ["project:replace-with-project-id"],
6
6
  "collect": {
7
7
  "enabled": true,
8
- "respect_ignore_file": ".omni-memory-ignore"
8
+ "respect_ignore_file": ".omnimem-ignore"
9
9
  }
10
10
  }