omnimem 0.1.5 → 0.1.6

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.6"
package/omnimem/webui.py CHANGED
@@ -74,6 +74,7 @@ HTML_PAGE = """<!doctype html>
74
74
  <div class=\"tabs\">
75
75
  <button class=\"tab-btn active\" data-tab=\"statusTab\" data-i18n=\"tab_status\">Status & Actions</button>
76
76
  <button class=\"tab-btn\" data-tab=\"configTab\" data-i18n=\"tab_config\">Configuration</button>
77
+ <button class=\"tab-btn\" data-tab=\"projectTab\" data-i18n=\"tab_project\">Project Integration</button>
77
78
  <button class=\"tab-btn\" data-tab=\"memoryTab\" data-i18n=\"tab_memory\">Memory</button>
78
79
  </div>
79
80
  </div>
@@ -121,6 +122,22 @@ HTML_PAGE = """<!doctype html>
121
122
  </div>
122
123
  </div>
123
124
 
125
+ <div id=\"projectTab\" class=\"panel\">
126
+ <div class=\"grid\">
127
+ <div class=\"card wide\">
128
+ <h3 data-i18n=\"project_title\">Project Integration</h3>
129
+ <label><span data-i18n=\"project_path\">Project Path</span><input id=\"projectPath\" placeholder=\"/path/to/your/project\" /></label>
130
+ <label><span data-i18n=\"project_id\">Project ID</span><input id=\"projectId\" placeholder=\"my-project\" /></label>
131
+ <div class=\"row-btn\">
132
+ <button id=\"btnProjectAttach\" data-i18n=\"btn_project_attach\">Attach Project + Install Agent Rules</button>
133
+ <button id=\"btnProjectDetach\" data-i18n=\"btn_project_detach\">Detach Project</button>
134
+ </div>
135
+ <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>
136
+ <pre id=\"projectOut\" class=\"small\"></pre>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
124
141
  <div id=\"memoryTab\" class=\"panel\">
125
142
  <div class=\"grid\">
126
143
  <div class=\"card wide\">
@@ -151,14 +168,18 @@ HTML_PAGE = """<!doctype html>
151
168
  const I18N = {
152
169
  en: {
153
170
  title: 'OmniMem WebUI', subtitle: 'Simple mode: Status & Actions / Configuration / Memory', language: 'Language',
154
- tab_status: 'Status & Actions', tab_config: 'Configuration', tab_memory: 'Memory',
171
+ tab_status: 'Status & Actions', tab_config: 'Configuration', tab_project: 'Project Integration', tab_memory: 'Memory',
155
172
  system_status: 'System Status', actions: 'Actions',
156
173
  btn_status: 'Check Sync Status', btn_bootstrap: 'Bootstrap Device Sync', btn_push: 'Push', btn_pull: 'Pull',
157
174
  btn_daemon_on: 'Enable Daemon', btn_daemon_off: 'Disable Daemon',
158
175
  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
176
  mem_recent: 'Recent Memories', mem_hint: 'Click an ID to open full content', mem_content: 'Memory Content',
160
177
  th_id: 'ID', th_layer: 'Layer', th_kind: 'Kind', th_summary: 'Summary', th_updated: 'Updated At',
178
+ project_title: 'Project Integration', project_path: 'Project Path', project_id: 'Project ID',
179
+ btn_project_attach: 'Attach Project + Install Agent Rules', btn_project_detach: 'Detach Project',
180
+ project_hint: 'Attach will create .omnimem files and inject managed memory protocol blocks into AGENTS.md / CLAUDE.md / .cursorrules.',
161
181
  cfg_saved: 'Configuration saved', cfg_failed: 'Save failed',
182
+ project_attach_ok: 'Project attached', project_detach_ok: 'Project detached', project_failed: 'Project action failed',
162
183
  init_ok: 'Config state: initialized', init_hint_ok: 'Daemon runs quasi-realtime sync in background (can be disabled).',
163
184
  init_missing: 'Config state: not initialized (save configuration first)', init_hint_missing: 'Daemon is disabled until configuration is initialized.',
164
185
  daemon_state: (d) => `Daemon: ${d.running ? 'running' : 'stopped'}, enabled=${d.enabled}, initialized=${d.initialized}`
@@ -363,6 +384,23 @@ HTML_PAGE = """<!doctype html>
363
384
  await loadDaemon();
364
385
  }
365
386
 
387
+ async function attachProject() {
388
+ const project_path = document.getElementById('projectPath').value.trim();
389
+ const project_id = document.getElementById('projectId').value.trim();
390
+ const out = document.getElementById('projectOut');
391
+ const d = await jpost('/api/project/attach', {project_path, project_id});
392
+ out.textContent = JSON.stringify(d, null, 2);
393
+ document.getElementById('status').innerHTML = d.ok ? `<span class=\"ok\">${t('project_attach_ok')}</span>` : `<span class=\"err\">${t('project_failed')}</span>`;
394
+ }
395
+
396
+ async function detachProject() {
397
+ const project_path = document.getElementById('projectPath').value.trim();
398
+ const out = document.getElementById('projectOut');
399
+ const d = await jpost('/api/project/detach', {project_path});
400
+ out.textContent = JSON.stringify(d, null, 2);
401
+ document.getElementById('status').innerHTML = d.ok ? `<span class=\"ok\">${t('project_detach_ok')}</span>` : `<span class=\"err\">${t('project_failed')}</span>`;
402
+ }
403
+
366
404
  function bindTabs() {
367
405
  const btns = document.querySelectorAll('.tab-btn');
368
406
  btns.forEach(btn => {
@@ -389,6 +427,8 @@ HTML_PAGE = """<!doctype html>
389
427
  document.getElementById('btnSyncPull').onclick = () => runSync('github-pull');
390
428
  document.getElementById('btnDaemonOn').onclick = () => toggleDaemon(true);
391
429
  document.getElementById('btnDaemonOff').onclick = () => toggleDaemon(false);
430
+ document.getElementById('btnProjectAttach').onclick = () => attachProject();
431
+ document.getElementById('btnProjectDetach').onclick = () => detachProject();
392
432
  }
393
433
 
394
434
  window.addEventListener('error', (e) => {
@@ -425,6 +465,137 @@ def _cfg_to_ui(cfg: dict[str, Any], cfg_path: Path) -> dict[str, Any]:
425
465
  }
426
466
 
427
467
 
468
+ def _upsert_managed_block(path: Path, block: str) -> None:
469
+ start = "<!-- OMNIMEM:START -->"
470
+ end = "<!-- OMNIMEM:END -->"
471
+ managed = f"{start}\n{block.rstrip()}\n{end}\n"
472
+ if path.exists():
473
+ old = path.read_text(encoding="utf-8")
474
+ if start in old and end in old:
475
+ left = old.split(start, 1)[0].rstrip()
476
+ right = old.split(end, 1)[1].lstrip()
477
+ new_text = f"{left}\n\n{managed}"
478
+ if right:
479
+ new_text += f"\n{right}"
480
+ path.write_text(new_text, encoding="utf-8")
481
+ return
482
+ sep = "\n\n" if old and not old.endswith("\n\n") else ""
483
+ path.write_text(old + sep + managed, encoding="utf-8")
484
+ return
485
+ path.write_text(managed, encoding="utf-8")
486
+
487
+
488
+ def _agent_protocol_block(project_id: str) -> str:
489
+ return (
490
+ "# OmniMem Memory Protocol\n"
491
+ "\n"
492
+ f"- Project ID: `{project_id}`\n"
493
+ "- Session start: run `omnimem brief --project-id <PROJECT_ID> --limit 8` and use it as active context.\n"
494
+ "- During task: when a stable decision/fact appears, run `omnimem write` with concise summary + evidence.\n"
495
+ "- Phase end: run `omnimem checkpoint` with goal/result/next-step/risks.\n"
496
+ "- If confidence is low or info is temporary, store in `instant`/`short`; promote to `long` only when repeated and stable.\n"
497
+ "- Never write raw secrets. Use credential references only (for example `op://...` or `env://...`).\n"
498
+ )
499
+
500
+
501
+ def _attach_project_in_webui(project_path: str, project_id: str, cfg_home: str) -> dict[str, Any]:
502
+ if not project_path:
503
+ return {"ok": False, "error": "project_path is required"}
504
+ project = Path(project_path).expanduser().resolve()
505
+ if not project.exists() or not project.is_dir():
506
+ return {"ok": False, "error": f"project path not found: {project}"}
507
+ if not project_id:
508
+ project_id = project.name
509
+
510
+ repo_root = Path(__file__).resolve().parent.parent
511
+ tpl = repo_root / "templates" / "project-minimal"
512
+ created: list[str] = []
513
+ updated: list[str] = []
514
+
515
+ files = [
516
+ (tpl / ".omnimem.json", project / ".omnimem.json"),
517
+ (tpl / ".omnimem-session.md", project / ".omnimem-session.md"),
518
+ (tpl / ".omnimem-ignore", project / ".omnimem-ignore"),
519
+ ]
520
+ for src, dst in files:
521
+ text = src.read_text(encoding="utf-8")
522
+ text = text.replace("replace-with-project-id", project_id)
523
+ text = text.replace("~/.omnimem", cfg_home or "~/.omnimem")
524
+ exists = dst.exists()
525
+ dst.write_text(text, encoding="utf-8")
526
+ (updated if exists else created).append(str(dst))
527
+
528
+ block = _agent_protocol_block(project_id=project_id)
529
+ managed_targets = [
530
+ project / "AGENTS.md",
531
+ project / "CLAUDE.md",
532
+ project / ".cursorrules",
533
+ ]
534
+ for fp in managed_targets:
535
+ exists = fp.exists()
536
+ _upsert_managed_block(fp, block)
537
+ (updated if exists else created).append(str(fp))
538
+
539
+ cursor_rule = project / ".cursor" / "rules" / "omnimem.mdc"
540
+ cursor_exists = cursor_rule.exists()
541
+ cursor_rule.parent.mkdir(parents=True, exist_ok=True)
542
+ cursor_rule.write_text(
543
+ (
544
+ "---\n"
545
+ "description: OmniMem project memory protocol\n"
546
+ "alwaysApply: true\n"
547
+ "---\n\n"
548
+ + block
549
+ ),
550
+ encoding="utf-8",
551
+ )
552
+ (updated if cursor_exists else created).append(str(cursor_rule))
553
+
554
+ return {
555
+ "ok": True,
556
+ "project_path": str(project),
557
+ "project_id": project_id,
558
+ "created": created,
559
+ "updated": updated,
560
+ }
561
+
562
+
563
+ def _detach_project_in_webui(project_path: str) -> dict[str, Any]:
564
+ if not project_path:
565
+ return {"ok": False, "error": "project_path is required"}
566
+ project = Path(project_path).expanduser().resolve()
567
+ if not project.exists() or not project.is_dir():
568
+ return {"ok": False, "error": f"project path not found: {project}"}
569
+
570
+ removed: list[str] = []
571
+ for name in [
572
+ ".omnimem.json",
573
+ ".omnimem-session.md",
574
+ ".omnimem-ignore",
575
+ ".cursorrules",
576
+ "CLAUDE.md",
577
+ "AGENTS.md",
578
+ ".cursor/rules/omnimem.mdc",
579
+ ]:
580
+ fp = project / name
581
+ if fp.exists():
582
+ txt = fp.read_text(encoding="utf-8", errors="ignore")
583
+ if "<!-- OMNIMEM:START -->" in txt and "<!-- OMNIMEM:END -->" in txt:
584
+ start = txt.index("<!-- OMNIMEM:START -->")
585
+ end = txt.index("<!-- OMNIMEM:END -->") + len("<!-- OMNIMEM:END -->")
586
+ new_txt = (txt[:start] + txt[end:]).strip()
587
+ if new_txt:
588
+ fp.write_text(new_txt + "\n", encoding="utf-8")
589
+ else:
590
+ fp.unlink()
591
+ removed.append(str(fp))
592
+ continue
593
+ if fp.name in {".omnimem.json", ".omnimem-session.md", ".omnimem-ignore", "omnimem.mdc"}:
594
+ fp.unlink()
595
+ removed.append(str(fp))
596
+ return {"ok": True, "project_path": str(project), "removed": removed}
597
+
598
+
428
599
  def run_webui(
429
600
  *,
430
601
  host: str,
@@ -516,6 +687,16 @@ def run_webui(
516
687
  self._send_json({"ok": True, **daemon_state})
517
688
  return
518
689
 
690
+ if parsed.path == "/api/project/defaults":
691
+ self._send_json(
692
+ {
693
+ "ok": True,
694
+ "project_path": "",
695
+ "project_id": "",
696
+ }
697
+ )
698
+ return
699
+
519
700
  if parsed.path == "/api/memories":
520
701
  q = parse_qs(parsed.query)
521
702
  limit = int(q.get("limit", ["20"])[0])
@@ -612,6 +793,26 @@ def run_webui(
612
793
  )
613
794
  return
614
795
 
796
+ if parsed.path == "/api/project/attach":
797
+ try:
798
+ out = _attach_project_in_webui(
799
+ project_path=str(data.get("project_path", "")).strip(),
800
+ project_id=str(data.get("project_id", "")).strip(),
801
+ cfg_home=str(cfg.get("home", "")).strip(),
802
+ )
803
+ self._send_json(out, 200 if out.get("ok") else 400)
804
+ except Exception as exc: # pragma: no cover
805
+ self._send_json({"ok": False, "error": str(exc)}, 500)
806
+ return
807
+
808
+ if parsed.path == "/api/project/detach":
809
+ try:
810
+ out = _detach_project_in_webui(str(data.get("project_path", "")).strip())
811
+ self._send_json(out, 200 if out.get("ok") else 400)
812
+ except Exception as exc: # pragma: no cover
813
+ self._send_json({"ok": False, "error": str(exc)}, 500)
814
+ return
815
+
615
816
  self._send_json({"ok": False, "error": "not found"}, 404)
616
817
 
617
818
  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.6",
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
  }